Cyber Docs

v0.4-dev 195-2fbe57c
Table of Contents

Introduction. #

Cyber is a fast, efficient, and concurrent scripting language. The landing page is at cyberscript.dev and contains performance metrics and release notes.

These docs provide a reference manual for the language. You can read it in order or jump around using the navigation. You may come across features that are marked Incomplete or Planned. This is because the docs are written as if all features have been completed already.

Type System. #

Cyber is a statically typed language so the documentation will provide examples using typed syntax. However, dynamic typing is also supported with a less restrictive syntax. If you're coming from a language such as Python, JavaScript, or Lua, it can be easier to get started with Dynamic Typing.

^topic

Hello World. #

use math

var worlds = ['World', '世界', 'दुनिया', 'mundo']
worlds.append(math.random())
for worlds -> w:
    print "Hello, $(w)!"
^topic

Syntax. #

^top

Cyber's syntax is concise and easy to read.

Statements. #

A statement ends with the new line:

var a = 123

To wrap a statement to the next line, the last token before the new line must be an operator or a separator:

var gameover = health <= 0 or
    player.collidesWith(spikes)

if year > 2020 and year <= 2030 and
    month > 0 and month <= 11:
    print 'Valid'

Any token inside a delimited syntax (such as parentheses or brackets) can be wrapped to the next line:

var sum = add(1, 2, 3, 4,
    100, 200, 300, 400)

var colors = ['red', 'blue', 'green',
    'purple', 'orange', 'yellow']
^topic

Blocks. #

Some statements can start a new block with a colon. The first statement in a new block must be indented further. Spaces or tabs can be used for indentation but not both.

-- This `if` statement begins a new block.
if true:
    var a = 234

Subsequent statements in the block must follow the same indentation. The block ends when a statement recedes from this indentation:

var items = [10, 20, 30]
for items -> it:
    if it == 20:
        print it
    print it      -- This is the first statement outside of the `if` block.

Compact blocks allow only one statement after the starting block:

-- A single line block.
if true: print 123

if true: print 123
    -- This is an indentation error since the compact block is already consumed.
    print 234

Since blocks require at least one statement, use pass as a placeholder statement:

func foo():
    pass
^topic

Variables. #

Variables allow values to be stored into named locations in memory.

Local variables and static variables are supported.

^topic

Local variables. #

Local variables exist until the end of their scope. They are declared and initialized using the var keyword:

var a = 123

When declared without a type specifier next to the variable, the type is inferred from the right initializer. In the above example, the variable a is initialized with the type int.

Variables can be assigned afterwards using the = operator:

a = 234
^topic

Explicit type constraint. #

When a type specifier is provided, the value assigned to the variable must satisfy the type constraint or a compile error is reported:

var a float = 123.0

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`.
^topic

Variable scopes. #

Blocks create a new variable scope. Variables declared in the current scope will take precedence over any parent variables with the same name:

func foo():
    var a = 234

    if true:
        var a = 345     -- New `a` declared.
        print a         --> 345

    print a             --> 234
^topic

Static variables. #

Static variables live until the end of the script. They act as global variables and are visible from anywhere in the script.

Static variables are declared with var like local variables, but a namespace must be provided before the variable name:

var .a = 123

func foo():
    print a     --> 123

The . prefix is used to reference the current module's namespace.

Unlike local variables, static variables do not currently infer the type from the right hand side so a specific type must be specified or it will default to the any type:

var .my_map Map = Map{}

Since static variables are initialized outside of a fiber's execution flow, they can not reference any local variables:

var .b = a   --> Compile error, initializer can not reference a local variable.

-- Main execution.
var a = 123

However, they can be reassigned after initialization:

var .b = 0

var a = 123
b = a            -- Reassigning after initializing.

Static variable initializers have a natural order based on when it was encountered by the compiler. In the case of imported variables, the order of the import would affect this order. The following would print '123' before '234':

var .a = print(123)
var .b = print(234)

When the initializers reference other static variables, those child references are initialized first in DFS order and supersede the natural ordering. The following initializes b before a.

var .a = b + 321
var .b = 123

print a        --> 444

Circular references in initializers are not allowed. When initialization encounters a reference that creates a circular dependency an error is reported.

var .a = b
var .b = a     --> CompileError. Referencing `a` creates a circular dependency.

Sometimes, you may want to initialize a static variable by executing multiple statements in order. For this use case, you can use a declaration block. Planned Feature

var .myImage =:
    var img = loadImage('me.png')
    img.resize(100, 100)
    img.filter(.blur, 5)
    break img

The final resulting value that is assigned to the static variable is provided by a break statement. If a break statement is not provided, none is assigned instead.

^topic

use $global #

When use $global is declared in the module, it allows the use of undeclared variables:

use $global

a = 123
print a    --> 123

Accessing an undeclared variable before it's initialized results in a runtime error:

use $global

print a    --> panic: `a` is not defined in `$global`.
^topic

Reserved identifiers. #

^topic

Keywords. #

There are 28 general keywords. This list categorizes them:

^topic

Contextual keywords. #

These keywords only have meaning in a certain context.

^topic

Literals. #

^topic

Operators. #

Cyber supports the following operators. They are ordered from highest to lowest precedence.

OperatorDescription
<< >>Bitwise left shift, right shift.
&Bitwise and.
| ||Bitwise or, exclusive or.
^Power.
/ % *Division, modulus, multiplication.
+ -Addition, subtraction.
asType casting.
> >= < <= != ==Greater, greater or equal, less, less or equal, not equals, equals.
andLogical and.
orLogical or.
.. -..Range, reversed range.
^topic

Arithmetic Operators. #

The following arithmetic operators are supported for the numeric data types.

1 + 2     -- Addition, evaluates to 3.
100 - 10  -- Subtraction, evaluates to 90.
3 * 4     -- Multiplication, evaluates to 12.
20 / 5    -- Division, evaluates to 4.
2 ^ 4     -- Raise to the power, evaluates to 16.
12 % 5    -- Modulus remainder, evaluates to 2.
-(10)     -- Apply negative, evaluates to -10.
^topic

Comparison Operators. #

Cyber supports the following comparison operators. By default, a comparison operator evaluates to a Boolean value.

The equals operator returns true if the two values are equal. For primitive types, the comparison checks the types and the underlying value. For strings, the underlying bytes are compared for equality. For objects, the comparison checks that the two values reference the same object.

1 == 1      -- Evaluates to `true`
1 == 2      -- Evaluates to `false`
1 == true   -- Evaluates to `false`

var a = 'abc'
a == 'abc'  -- Evaluates to `true`

a = []
var b = a
a == b      -- Evaluates to `true`
a == []     -- Evaluates to `false`

The not equals operator returns true if the two values are not equal.

1 != 1      -- Evaluates to `false`
1 != 2      -- Evaluates to `true`

Number types have additional comparison operators.

a > b    -- `true` if a is greater than b
a >= b   -- `true` if a is greater than or equal to b
a < b    -- `true` if a is less than b
a <= b   -- `true` if a is less than or equal to b
^topic

Logic Operators. #

The logical operators and, or, and not are supported.

and evaluates to true if both operands are true. Otherwise, it evaluates to false. If the left operand is false, the evaluation of the right operand is skipped:

true and true    --> true
true and false   --> false
false and true   --> false
false and false  --> false

or evaluates to true if at least one of the operands is true. Otherwise, it evaluates to false. If the left operand is true, the evaluation of the right operand is skipped:

true or true     --> true
true or false    --> true
false or true    --> true
false or false   --> false

The unary operator not performs negation on the boolean value. The unary operator ! can also be used instead of not.

not false     --> true
not true      --> false
!false        --> true
!true         --> false
^topic

Bitwise Operators. #

The following bitwise operators are supported for int number values.

-- Bitwise and: any underlying bits that are set in both integers are set in the new integer.
a & b

-- Bitwise or: any underlying bits that are set in either integer a or integer b are set in the new integer.
a | b

-- Bitwise exclusive or: any underlying bits that are set in either integer a or integer b but not both are set in the new integer.
a || b

-- Bitwise right shift: a's bits are shifted b bits to the least significant end. This performs sign-extension on the 32-bit integer.
a >> b

-- Bitwise left shift: a's bits are shifted b bits to the most significant end. This does not perform sign-extension on the 32-bit integer.
a << b

-- Bitwise not: a's integer bits are flipped.
~a
^topic

Operator overloading. #

See Operator overloading in Metaprogramming.

^topic

Zero values. #

Uninitialized type fields currently default to their zero values. However, this implicit behavior will be removed in the future in favor of a default value clause. Zero values must then be expressed using the reserved zero literal.

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

TypeZero value
booleanfalse
int0
float0.0
String''
ArrayArray('')
List[]
MapMap{}
type SS{}
#host type SS.$zero()
dynamicint(0)
anyint(0)
?SOption(S).none
^topic

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
^topic

Comments. #

A single line comment starts with two hyphens and ends at the end of the line.

-- This is a comment.

var a = 123   -- This is a comment on the same line as a statement.

There will be multi-line comments in Cyber but the syntax has not been determined.

^topic

CYON. #

CYON or the Cyber object notation is similar to JSON. The format uses the same literal value semantics as Cyber.

{
    name: 'John Doe',
    'age': 25,
    -- This is a comment
    cities: [
        'New York',
        'San Francisco',
        'Tokyo',
    ],
}
^topic

Basic Types. #

^top

In Cyber, there are primitive types and object types. By default, primitives are copied around by value and don't need additional heap memory or reference counts.

Primitives include Booleans, Floats, Integers, Enums, Symbols, and Error Values.

Object types include Lists, Tuples, Maps, Strings, Arrays, Objects, Lambdas, Fibers, Choices, Optionals, Pointers, and several internal object types.

Booleans. #

Booleans can be true or false. See type bool.

var a = true
if true:
    print 'a is true'
^topic

Numbers. #

^topic

Integers. #

int is the default integer type. It has 48-bits and can represent integers in the range -(247) to 247-1. See type int.

When a numeric literal is used and the type can not be inferred, it will default to the int type:

var a = 123

Integer notations always produce a int value:

var a = 0xFF     -- hex.
a = 0o17         -- octal.
a = 0b1010       -- binary.
a = `🐶`         -- UTF-8 rune.

Arbitrary values can be converted to a int using the type as a function.

var a = '123'
var b = int(a) 

In addition to arithmetic operations, integers can also perform bitwise operations.

^topic

Floats. #

float is the default floating point type. It has a (IEEE 754) 64-bit floating point format. See type float.

Although a float represents a decimal number, it can also represent integers between -(253-1) and (253-1). Any integers beyond the safe integer range is not guaranteed to have a unique representation.

A numeric literal can be used to create a float if the inferred type is a float:

var a float = 123

Decimal and scientific notations always produce a float value:

var a = 2.34567
var b = 123.0e4

Arbitrary values can be converted to a float using the type as a function.

var a = '12.3'
var b = float(a) 
^topic

Big Numbers. #

Planned Feature

^topic

Strings. #

The String type represents a sequence of UTF-8 codepoints. Each code point is stored internally as 1-4 bytes. See type String.

Strings are not validated by default. If the codepoint is invalid at a particular index, the replacement character (0xFFFD) is returned instead.

Strings are immutable, so operations that do string manipulation return a new string. By default, short strings are interned to reduce memory footprint.

Under the hood, there are multiple string implementations to make operations faster by default using SIMD.

^topic

Raw string literal. #

A raw string doesn't allow any escape sequences or string interpolation.

Single quotes are used to delimit a single line literal:

var fruit = 'apple'
var str = 'abc🦊xyz🐶'

Since raw strings interprets the sequence of characters as is, a single quote character can not be escaped:

var fruit = 'Miso's apple'    -- ParseError.

Triple single quotes are used to delimit a multi-line literal. It also allows single quotes and double single quotes in the string:

var fruit = '''Miso's apple'''
var greet = '''Hello
World'''
^topic

String literal. #

A string literal allows escape sequences and string interpolation.

Double quotes are used to delimit a single line literal:

var fruit = "apple"
var sentence = "The $(fruit) is tasty."
var doc = "A double quote can be escaped: \""

Triple double quotes are used to delimit a multi-line literal:

var title = "last"
var doc = """A double quote " doesn't need to be escaped."""
var str = """line a
line "b"
line $(title)
"""
^topic

Escape sequences. #

The following escape sequences are supported in string literals wrapped in double quotes:

SequenceCodeCharacter
\00x00Null
\a0x07Terminal bell
\b0x08Backspace
\e0x1bEscape
\n0x0aLine feed
\r0x0dCarriage return
\t0x09Horizontal tab
\"0x22Double quote
\\0x5cBackslash
\x??--Hex number

Example:

print "\xF0\x9F\x90\xB6"    --> 🐶
^topic

String indexing. #

The index operator returns the rune starting at the given byte index:

var a = 'abcxyz'
print a[1]          --> 98
print(a[1] == `b`)  --> true

If an index does not begin a sequence of valid UTF-8 bytes, the replacement character (0xFFFD, 65533) is returned:

var a = '🐶abcxyz'
print a[1]          --> 65533

Since indexing operates at the byte level, it should not be relied upon for iterating runes or rune indexing. However, if the string is known to only contain ASCII runes (each rune occupies one byte), indexing will return the expected rune.

String.seek will always return the correct byte index for a rune index:

var a = '🐶abcxyz'
print a.seek(2)         --> 5
print a[a.seek(2)]      --> 98 (`b`)

Similarily, slicing operates on byte indexes. Using the slice operator will return a view of the string at the given start and end (exclusive) indexes. The start index defaults to 0 and the end index defaults to the string's byte length:

var str = 'abcxyz'
var sub = str[0..3]
print str[0..3]  --> abc
print str[..5]   --> abcxy
print str[1..]   --> bcxyz
^topic

String concatenation. #

Concatenate two strings together with the + operator or the method concat.

var res = 'abc' + 'xyz'
res = res.concat('end')
^topic

String interpolation. #

Expressions can be embedded into string templates with $():

var name = 'Bob'
var points = 123
var str = "Scoreboard: $(name) $(points)"

String templates can not contain nested string templates.

^topic

String formatting. #

Values that can be formatted into a string will have a fmt method:

var file = os.openFile('data.bin', .read)
var bytes = file.readToEnd()

-- Dump contents in hex.
print "$(bytes.fmt(.x))"
^topic

Line-join literal. #

The line-join literal joins string literals with the new line character \n. Planned Feature

This has several properties:

var paragraph = [
    \'the line-join literal
    \'hello\nworld
    \"hello $(name)
    \'last line
    \'
]
^topic

Mutable strings. #

To mutate an existing string, use type MutString. Planned Feature

^topic

Arrays. #

An Array is an immutable sequence of bytes. See type Array.

var a = Array('abcd')
a = a.insertByte(1, 255)
print a[0]     -- "97"
print a[1]     -- "255"
^topic

Lists. #

Lists are a builtin type that holds an ordered collection of elements. Lists grow or shrink as you insert or remove elements. See type List.

-- Construct a new list.
var list = [1, 2, 3]

-- The first element of the list starts at index 0.
print list[0]    -- Prints '1'

Lists can be sliced with the range .. clause. The sliced list becomes a new list that you can modify without affecting the original list. The end index is non-inclusive.

var list = [ 1, 2, 3, 4, 5 ]
list[0..0]    -- []          
list[0..3]    -- [ 1, 2, 3 ] 
list[3..]     -- [ 4, 5 ]    
list[..3]     -- [ 1, 2, 3 ] 

The +.. invokes the slice operator with an end position that is an increment from the start: Planned Feature

var list = [ 1, 2, 3, 4, 5 ]
list[2+..2]   -- [ 3, 4 ]

List operations.

var list = [234]

-- Append a value.
list.append(123)

-- Inserting a value at an index.
list.insert(1, 345)

-- Get the length.
print list.len()  -- Prints '2'

-- Sort the list in place.
list.sort((a, b) => a < b)

-- Iterating a list.
for list -> it:
    print it

-- Remove an element at a specific index.
list.remove(1)
^topic

Tuples. #

Incomplete: Tuples can only be created from #host funcs at the moment.

^topic

Tables. #

A Table is a versatile object that can have an arbitrary set of fields.

By default, the record literal initializes a Table:

var o = {}

o = {a: 123}
print o.a          --> 123

A Table can be initialized explicitly using its type name:

var o = Table{a: 123}

Any field can be assigned a value. However, accessing a field before it's initialized results in a panic:

o.my_field = 234
print o.my_field   --> 234

print o.foo        --> panic: The field `foo` was not initialized.
^topic

Table indexing. #

Indexing can be used to access a dynamic field or an arbitrary key:

var o = { name: 'Nova' }
var field = 'name'
print o[field]     --> Nova

o[10] = [1, 2, 3]
print o[10]        --> List (3)

If the key is not an identifier string, the value can only be obtained through indexing.

^topic

Check field existence. #

Planned Feature

^topic

Prototypes. #

Planned Feature

^topic

Maps. #

Maps are a builtin type that store key value pairs in dictionaries. See type Map.

Maps are initialized with the Map type and a record literal:

var map = Map{ a: 123, b: () => 5 }

The empty record literal creates an empty map:

var empty = Map{}
^topic

Map indexing. #

Get a value from the map using the index operator:

print map['a']
^topic

Map operations. #

var map = Map{}

-- Set a key value pair.
map[123] = 234

-- Get the size of the map.
print map.size()

-- Remove an entry by key.
map.remove(123)

-- Iterating a map.
for map -> [val, key]:
    print "$(key) -> $(val)"
^topic

Map block. #

Entries can also follow a collection literal block. This gives structure to the entries and has the added benefit of allowing multi-line lambdas. Most likely this will not be implemented in favor of a builder syntax

var colors = {}:
    red: 0xFF0000
    green: 0x00FF00
    blue: 0x0000FF
    dump func (c):
        print c.red
        print c.green
        print c.blue

    -- Nested map.
    darker {}: 
        red: 0xAA0000
        green: 0x00AA00
        blue: 0x0000AA
^topic

Symbols. #

Symbol literals begin with ., followed by an identifier. They have their own global unique id.

var currency = .usd
print(currency == .usd)   -- 'true'
print int(currency)       -- '123' or some arbitrary id.
^topic

any. #

Unlike dynamic, any is statically typed and performs type checks at compile-time. 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)    --> 100
^topic

dynamic. #

The dynamic type defers type checking to runtime. However, it also tracks its own recent type in order to surface errors at compile-time. See Dynamic Typing.

^topic

Custom Types. #

^top

Objects. #

An object type contains field and function members. New instances can be created from them similar to a struct or class in other languages. Unlike classes, there is no builtin concept of inheritance but a similar effect can be achieved using composition and embedding.

Object types are declared with type object. The object keyword is optional when using a type declaration. These two declarations are equivalent:

type A object:
    my_field int

type A:
    my_field int

Fields must be declared at the top of the type block with their names and type specifiers:

type Node:
    value int
    next  ?Node
^topic

Instantiate. #

New object instances are created using a record literal with a leading type name:

var node = Node{value: 123, next: none}
print node.value       -- Prints "123"

A record literal can also initialize to the inferred object type:

var node Node = {value: 234, next: none}
print node.value       -- Prints "234"
^topic

Field visibility. #

All fields have public visibility. However, when a field is declared with a - prefix, it suggests that it should not be made available to an editor's autocomplete:

type Info:
    a       int
    -b      int
    -secret String
^topic

Default field values. #

When a field is omitted in the record literal, it gets initialized to its zero value:

var node Node = {value: 234}
print node.next       -- Prints "Option.none"

type Student:
    name String
    age  int
    gpa  float

var s = Student{}
print s.name       -- Prints ""
print s.age        -- Prints "0"
print s.gpa        -- Prints "0.0"
^topic

Circular references. #

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

type Node:
    val  any
    next ?Node

var n = Node{}    -- Initializes.

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:
    val  any
    next Node

var n = Node{}    -- CompileError. Can not zero initialize `next`
                   -- because of circular dependency.
^topic

Unnamed object. #

Unnamed object types can be declared and used without an explicit type declaration:

type Node:
    value object:
        a  int
        b  float
    next  ?Node

var node = Node{
    value: {a: 123, b: 100.0},
    next: none,
}
^topic

Methods. #

Methods allow invoking a function on an object instance using the . operator:

type Node:
    value int
    next  ?Node

    func inc(n):
        value += n

    func incAndPrint():
        self.inc(321)
        print value

var n = Node{value: 123, next: none}
n.incAndPrint()         -- Prints "444"

Methods can be declared outside of the type declaration. When using the flat declaration style, self must be the first parameter to distinguish it from a type function:

func Node.getNext(self):
    return self.next
^topic

self variable. #

Type members can be implicitly referenced inside the method. Incomplete: Only the type's fields can be referenced this way.

To reference members explicitly inside a method, use the builtin self:

type Node:
    value int
    next  ?Node

    func double():
        return self.value * 2
^topic

Type functions. #

Type functions are declared outside of the type block with an explicit namespace path:

type Node:
    value int
    next  ?Node

-- Declare static function inside `Node`.
func Node.new():
    return Node{value: 123, next: none}

var n = Node.new()
^topic

Type variables. #

Similarily, type variables are declared outside of the type block:

-- Declare inside the `Node` namespace.
var Node.DefaultValue = 100

print Node.DefaultValue    -- Prints "100"
^topic

Type embedding. #

Type embedding facilitates type composition by using the namespace of a child field's type: Planned Feature

type Base:
    a int

    func double() int:
        return a * 2

type Container:
    b use Base

var c = Container{b: Base{a: 123}}
print c.a         --> 123
print c.double()  --> 246

Note that embedding a type does not declare extra fields or methods in the containing type. It simply augments the type's using namespace by binding the embedding field.

If there is a member name conflict, the containing type's member has a higher precedence:

type Container:
    a int
    b use Base

var c = Container{a: 999, b: Base{a: 123}}
print c.a         --> 999
print c.double()  --> 246

Since the embedding field is named, it can be used just like any other field:

print c.b.a       --> 123
^topic

Structs. #

Struct types can contain field and method members just like object types, but their instances are copied by value rather than by reference. In that sense, they behave like primitive data types.

Unlike objects, structs do not have a reference count. They can be safely referenced using borrow semantics. Unsafe pointers can also reference structs.

^topic

Declare struct. #

Struct types are created using the type struct declaration:

type Vec2 struct:
    x float
    y float

var v = Vec2{x: 30, y: 40}
^topic

Copy structs. #

Since structs are copied by value, assigning a struct to another variable creates a new struct:

var v = Vec2{x: 30, y: 40}
var w = v
v.x = 100
print w.x    -- Prints '30'
print v.x    -- Prints '100'
^topic

Enums. #

A new enum type can be declared with the type enum declaration. An enum value can only be one of the unique symbols declared in the enum type. By default, the symbols generate unique ids starting from 0.

type Fruit enum:
    case apple
    case orange
    case banana
    case kiwi

var fruit = Fruit.kiwi
print fruit       -- 'Fruit.kiwi'
print int(fruit)  -- '3'

When the type of the value is known to be an enum, it can be assigned using a symbol literal.

var fruit = Fruit.kiwi
fruit = .orange
print(fruit == Fruit.orange)   -- 'true'
^topic

Choices. #

Choices are enums with payloads (also known as sum types or tagged unions). An enum declaration becomes a choice type if one of the cases has a payload type specifier:

type Shape enum:
    case rectangle Rectangle
    case circle    object:
        radius float
    case triangle  object:
        base   float
        height float
    case line      float
    case point 

type Rectangle:
    width  float
    height float
^topic

Initialize choice. #

If the payload is an object type, the choice can be initialized with a simplified record literal:

var s = Shape.rectangle{width: 10, height: 20}

The general way to initialize a choice is to pass the payload as an argument:

var rect = Rectangle{width: 10, height: 20}
var s = Shape{rectangle: rect}

s = Shape{line: 20}

A choice without a payload is initialized with an empty record literal:

var s = Shape.point{}
^topic

Choice switch. #

case clauses can match choices and capture the payload:

switch s
case .rectangle -> r:
    print "$(r.width) $(r.height)"
case .circle -> c:
    print "$(c.radius)"
case .triangle -> t:
    print "$(t.base) $(t.height)"
case .line -> len:
    print "$(len)"
case .point:
    print "a point"
else:
    print "Unsupported."
^topic

Access choice. #

A choice can be accessed by specifying the access operator .! before the tagged member name. This will either return the payload or panic at runtime: Planned Feature

var s = Shape{line: 20}
print s.!line     -- Prints '20'
^topic

Optionals. #

Optionals provide Null Safety by forcing values to be unwrapped before they can be used.

The generic Option type is a choice type that either holds a none value or contains some value. The option template is defined as:

template(T type)
type Option enum:
    case none
    case some #T

A type prefixed with ? is the idiomatic way to create an option type. The following String optional types are equivalent:

Option(String)
?String
^topic

Wrap value. #

A value is automatically wrapped into the inferred optional's some case:

var a ?String = 'abc'
print a     --> some(abc)'
^topic

Wrap none. #

none is automatically initialized to the inferred optional's none case:

var a ?String = none
print a     --> none
^topic

Unwrap or panic. #

The .? access operator is used to unwrap an optional. If the expression evaluates to the none case, the runtime panics:

var opt ?int = 123
var v = opt.?
print v     --> 123
^topic

Unwrap or default. #

The ?else control flow operator either returns the unwrapped value or a default value when the optional is none:

var opt ?int = none
var v = opt ?else 123
print v     --> 123

?else can be used in an assignment block: Planned Feature

var v = opt ?else:
    break 'empty'

var v = opt ?else:
    throw error.Missing
^topic

Optional chaining. #

Given the last member's type T in a chain of ?. access operators, the chain's execution will either return Option(T).none on the first encounter of none or returns the last member as an Option(T).some: Planned Feature

print root?.a?.b?.c?.last
^topic

if unwrap. #

The if statement can be amended to unwrap an optional value using the capture -> operator:

var opt ?String = 'abc'
if opt -> v:
    print v     -- Prints 'abc'
^topic

while unwrap. #

The while statement can be amended to unwrap an optional value using the capture -> operator. The loop exits when none is encountered:

var iter = dir.walk()
while iter.next() -> entry:
    print entry.name
^topic

Type aliases. #

A type alias refers to a different target type. Once declared, the alias and the target type can be used interchangeably.

A type alias is declared using the assignment = operator after a type name declaration:

type Vec2:
    x float
    y float

type Pos2 = Vec2

var pos = Pos2{x: 3, y: 4}
^topic

Distinct types. #

A distinct type creates a new type by copying a target type.

It's declared with type name declaration followed by the target type specifier:

type Vec2:
    x float
    y float

type Pos2 Vec2

var pos = Pos2{x: 3, y: 4}

Functions can be declared under the new type's namespace:

use math

type Pos2 Vec2:
    func blockDist(o Pos2):
        var dx = math.abs(o.x - x)
        var dy = math.abs(o.y - y)
        return dx + dy

var pos = Pos2{x: 3, y: 4}
var dst = Pos2{x: 4, y: 5}
print pos.blockDist(dst)     --> 2

Note that functions declared from the target type do not carry over to the new type.

Unlike a type alias, the new type and the target type can not be used interchangeably since they are different types. However, instances of the new type can be casted to the target type, and vice versa: Planned Feature

type Pos2 Vec2

var a = Pos2{x: 3, y: 4}

var b Vec2 = a as Vec2
^topic

Traits. #

Planned Feature

^topic

Union types. #

Planned Feature

^topic

Generic types. #

Templates are used to specialize type declarations. Since template parameters only exist at compile-time, the # prefix is used to reference them in the template body:

template(T type)
type MyContainer:
    id    int
    value #T

    func get() #T:
        return self.value
^topic

Expand type template. #

When the template is invoked with compile-time argument(s), a specialized version of the type is generated.

In this example, String can be used as an argument since it satisfies the type parameter constraint:

var a MyContainer(String) = {id: 123, value: 'abc'}
print a.get()      -- Prints 'abc'

Note that invoking the template again with the same argument(s) returns the same generated type. In other words, the generated type is always memoized from the input parameters.

^topic

Control Flow. #

^top

Cyber provides the common constructs to branch and enter loops.

Branching. #

^topic

if statement. #

Use if and else statements to branch the execution of your code depending on conditions. The else clause can contain a condition which is only evaluated if the previous if or conditional else clause was false.

var a = 10
if a == 10:
    print 'a is 10'
else a == 20:
    print 'a is 20'
else:
    print 'neither 10 nor 20'
^topic

if expression. #

An if expression evaluates a condition in parentheses and returns either the true value or false value. Unlike the if statement, the if expression can not contain else conditional cases:

var a = 10
var str = if (a == 10) 'red' else 'blue'
^topic

and/or #

Use and and or logical operators to combine conditions:

var a = 10
if a > 5 and a < 15:
    print 'a is between 5 and 15'
if a == 20 or a == 10: 
    print 'a is 10 or 20'
^topic

Iterations. #

^topic

Infinite while. #

The while keyword starts an infinite loop which continues to run the code in the block until a break or return is reached.

var count = 0
while:
    if count > 100:
        break
    count += 1
^topic

Conditional while. #

When the while clause contains a condition, the loop continues to run until the condition is evaluated to false:

var running = true
var count = 0
while running:
    if count > 100:
        running = false
    count += 1
^topic

for range. #

for loops can iterate over a range that starts at an int (inclusive) to a target int (exclusive). The capture operator -> is used to capture the loop's counter variable:

for 0..4:
    performAction() 

for 0..100 -> i:
    print i    -- 0, 1, 2, ... , 99

To decrement the counter instead, use -..:

for 100-..0 -> i:
    print i    -- 100, 99, 98, ... , 1

When the range operator .. is replaced with ..=, the target int is inclusive: Planned Feature

for 0..=100 -> i:
    print i    -- 0, 1, 2, ... , 100
^topic

for each. #

The for clause can iterate over any type that implements the Iterable trait. An Iterable contains an iterator() method which returns an Iterator object. The for loop continually invokes the Iterator's next() method until none is returned.

Lists can be iterated since they implement the Iterable trait. The capture operator -> is used to capture the value returned from an iterator's next():

var list = [1, 2, 3, 4, 5]

for list -> n:
    print n

Maps can be iterated. next() returns a key and value tuple:

var map = Map{ a: 123, b: 234 }

for map -> entry:
    print entry[0]
    print entry[1]

Use the destructure syntax to extract the key and value into two separate variables:

for map -> [ key, val ]:
    print "key $(key) -> value $(val)"
^topic

for each with index. #

A counting index can be declared after the each variable. The count starts at 0 for the first value:

var list = [1, 2, 3, 4, 5]

for list -> val, i:
    print "index $(i), value $(val)"
^topic

Exit loop. #

Use break to exit a loop. This loop stops printing once i reaches 4:

for 0..10 -> i:
    if i == 4:
        break
    print i
^topic

Next iteration. #

Use continue to skip the rest of the loop and go to the next iteration. This loop prints 0 through 9 but skips 4:

for 0..10 -> i:
    if i == 4:
        continue
    print i
^topic

switch matching. #

The switch statement branches to a case block from a matching case condition. The expression that is matched against comes after switch statement. Multiple cases can be grouped together using a comma separator. An optional else fallback case is executed when no other cases were matched. Incomplete: Not all types can be used in the case conditions such as ranges.

var val = 1000
switch val
case 0..100: print 'at or between 0 and 99'
case 100   : print 'val is 100'
case 200:
    print 'val is 200'
case 300, 400:
    print 'combined case'
else:
    print "val is $(val)"

The switch statement requires at least one case block or an else block. When the switch statement begins a new block, the case statements must be indented:

switch val:
    case 0: print 'a'
    case 1: print 'b'
    else: print 'c'
^topic

switch assignment. #

Although switch can not be used freely as an expression, it can be assigned to a left variable or destination:

var shu = switch pepper:
    case 'bell'     => 0
    case 'anaheim'  => 500
    case 'jalapeño' => 2000
    case 'serrano'  => 10000

When declaring an assignable switch, the cases must have a return value using the syntax case {cond} => {expr} or else => {expr}.

^topic

switch break. #

A break statement exits the current case block and resumes execution after the end of the switch statement: Planned Feature

switch value
case 0..5:
    print value
    if value == 3:
        break case
    print value    -- Skips second print if `value` is 3.
^topic

Try/Catch. #

The try catch statement, try else and try expressions provide a way to catch a throwing error and resume execution in a different branch. Learn more about Error Handling.

^topic

Deferred Execution. #

Planned Feature

^topic

Functions. #

^top

In Cyber, there are first-class functions (or function values) and static functions.

Static functions. #

Functions are declared with the func keyword and must have a name.

use math

func dist(x0 float, y0 float, x1 float, y1 float) float:
    var dx = x0-x1
    var dy = y0-y1
    return math.sqrt(dx^2 + dy^2)

Functions can be invoked with arguments:

print dist(0, 0, 10, 20)

Functions are initially static, but they can be passed around as a lambda or assigned to a variable:

-- Assigning to a local variable.
var bar = dist

func squareDist(dist dynamic, size float) float:
    return dist(0.0, 0.0, size, size)
    
-- Passing `dist` as an argument.
print squareDist(dist, 30.0)

Functions can only return one value. However, the value can be destructured: Planned Feature

use {cos, sin} 'math'

func compute(rad float) [float, float]:
    return [ cos(rad), sin(rad) ]

var [ x, y ] = compute(pi)
^topic

Function overloading. #

Functions can be overloaded by their type signature:

func foo() int:
    return 2 + 2

func foo(n int) int:
    return 10 + n

func foo(n int, m int) int:
    return n * m

print foo()         --> 4
print foo(2)        --> 12
print foo(20, 5)    --> 100
^topic

Lambdas. #

Lambdas or function values can be assigned to variables or passed as arguments into other constructs.

When a lambda only returns an expression, it can be declared with a simplified syntax.

-- Passing simple lambda as an argument.
foo(word => toUpper(word))

-- A simple lambda with multiple arguments.
foo((word, prefix) => prefix + toUpper(word))

-- Assigning a simple lambda.
canvas.onUpdate = delta_ms => print delta_ms

Lambdas that need a block of statements can be declared with the func keyword without a name.

-- Assigning lambda block to a variable.
var add = func (a int, b int) int:
    return a + b

-- Passing a lambda block as an argument.
canvas.onUpdate():
    ..func (delta_ms):
        print delta_ms

Passing a lambda block as a call argument is only possible in a call block. Planned Feature See Function calls.

^topic

Closures. #

Lambdas can capture local variables from parent blocks. This example shows the lambda f capturing a from the main scope: Incomplete, only variables one parent block away can be captured.

var a = 1
var f = func() int:
    return a + 2
print f()         --> 3

The following lambda expression captures a from the function add:

func add():
    var a = 123
    return b => a + b
var addTo = add()
print addTo(10)   --> 133

Like static variables, static functions can not reference local variables outside of their scope:

var a = 1
func foo():
    print a       --> CompileError: Undeclared variable `a`.
^topic

Named parameters. #

Planned Feature

^topic

Optional parameters. #

Planned Feature

^topic

Variadic parameters. #

Planned Feature

^topic

Function calls. #

The straightforward way to call a function is to use parentheses.

var d = dist(100, 100, 200, 200)

You can call functions with named parameters.

Planned Feature

var d = dist(x0: 10, x1: 20, y0: 30, y1: 40)
^topic

Shorthand syntax. #

Functions can be invoked without parentheses. This only works for calls that take at least one argument. Named arguments are not allowed and nested function calls must use parentheses:

-- Calls the function `dist`.
var d = dist 100, 100, 200, 200

func foo():       
    return 4

var r = foo       -- Returns the function itself as a value.
                     -- Does not call the function `random`.
r = foo()         -- Calls the function `random`.
^topic

Call block syntax. #

The call expression block continues to add arguments from the block's body. If arguments are omitted from the initial call expression they can be added inside using the .. syntax. Arguments mapped to named parameters have a key value syntax separated by a :. All other arguments are added into a list and passed as the last argument.

Planned Feature

foo(123):
    ..func ():
        return 123
    param3: 123
    234
    bar()
    'hello'

In the example above, the function foo is called with 4 arguments. The first argument 123 is included in the starting call expression. The second argument is a function value inside the call expression block. The third argument is mapped to the param param3. Finally, the fourth argument is a list that contains 234, bar(), and 'hello'.

^topic

Generic functions. #

Planned Feature

^topic

Modules. #

^top

Modules have their own namespace and contain accessible static symbols. By default, importing another Cyber script returns a module with its declared symbols.

Importing. #

When a use declaration contains only a single identifier, it creates a local alias to a module using the identifier as the module specifier. Cyber's CLI comes with some builtin modules like math and test.

use test
test.eq(123, 123)

use math
print math.cos(0)

The explicit import syntax requires an alias name followed by a module specifier as a raw string:

use m 'math'
print m.random()

When Cyber is embedded into a host application, the module resolver and loader can be overridden using libcyber.

^topic

Import file. #

File modules can be imported:

-- Importing a module from the local directory.
use b 'bar.cy'
print b.myFunc()
print b.myVar
^topic

Import URL. #

Modules can be imported over the Internet:

-- Importing a module from a CDN.
use rl 'https://mycdn.com/raylib'

When importing using a URL without a file name, Cyber's CLI will look for a mod.cy from the path instead.

^topic

Import all. #

If the alias name is the wildcard character, all symbols from the module are imported into the using namespace: This feature is experimental and may be removed in a future version.

use * 'math'
print random()
^topic

Main module. #

Only the main module can have top-level statements that aren't static declarations. If an imported module contains top-level statements, they are ignored. In the future, it may result in a compile error instead:

-- main.cy
use a 'foo.cy'
print a.foo

-- foo.cy
use 'bar.cy'
var .foo = 123
print foo         -- Statement is ignored.

-- bar.cy
var .bar = 321
print bar         -- Statement is ignored.
^topic

Circular imports. #

Circular imports are allowed. In the following example, main.cy and foo.cy import each other without any problems.

-- main.cy
use foo 'foo.cy'

func printB():
    foo.printC()

foo.printA()

-- foo.cy
use main 'main.cy'

func printA():
    main.printB()

func printC():
    print 'done'

Static variable declarations from imports can have circular references. Read more about this in Static variables.

^topic

Destructure import. #

Modules can also be destructured using the following syntax:

Planned Feature

use { cos, pi } 'math'
print cos(pi)
^topic

Exporting. #

All symbols are exported when the script's module is loaded. However, symbols can be declared with a hidden modifier.

func foo():         -- Exported static function.
    print 123

var .bar = 234      -- Exported static variable.

type Thing:  -- Exported type.
    var a float
^topic

Module URI. #

To get the absolute path of the current module, reference the compile-time constant #modUri. This can be used with os.dirName to get the current module directory.

print #modUri              -- Prints '/some/path/foo.cy'

use os
print os.dirName(#modUri)  -- Prints '/some/path'
^topic

Symbol visibility. #

All symbols have public visibility. However, when a symbol is declared with a - prefix, it suggests that it should not be made available to an editor's autocomplete:

-type Foo:
    a int
    b int

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

Furthermore, the symbol is excluded when its module is included using use *.

^topic

Symbol alias. #

use can be used to create an alias to another symbol:

use eng 'lib/engine.cy'
use Vec2 -> eng.Vector2
^topic

Builtin modules. #

Builtin modules are the bare minimum that comes with Cyber. The embeddable library contains these modules and nothing more. They include:

^topic

module core #

The core module contains functions related to Cyber and common utilities. It is automatically imported into each script's namespace.

Sample usage:

-- `print` and `typeof` are available without imports.
print 'hello'
print typeof('my str').id()

^topic

func copy(val any) any

Copies a primitive value or creates a shallow copy of an object value.

func dump(val any)

Prints the result of toCyon on a value.

func eprint(str any)

Prints a value to the error stream. The host determines how it is printed.

func errorReport() String

func getObjectRc(val any) int

Returns the current reference count of an object.

func is(a any, b any) bool

Returns whether two values refer to the same instance.

func isAlpha(val int) bool

Returns whether a rune is an alphabetic letter.

func isDigit(val int) bool

Returns whether a rune is a digit.

func isNone(val any) bool

Returns whether a boxed value is the none case of a generic Option(T) type. This is temporary and may be removed in a future release.

func must(val any) any

If val is an error, panic(val) is invoked. Otherwise, val is returned.

func panic(err any) dynamic

Stop execution in the current fiber and starts unwinding the call stack. See Unexpected Errors.

func performGC() Map

Runs the garbage collector once to detect reference cycles and abandoned objects. Returns the statistics of the run in a map value.

func print(str any)

Prints a value. The host determines how it is printed.

func runestr(val int) String

Converts a rune to a string.

func typeof(val any) metatype

Returns the value's type as a metatype object.

^topic

type void #

^topic

type bool #

func bool.$call(val any) bool

Converts a value to either true or false.

^topic

type symbol #

^topic

type error #

func sym() symbol

Return the underlying symbol.

func error.$call(val any) error

Create an error from an enum or symbol.

^topic

type int #

func $prefix~() int

func $prefix-() int

func $infix<(o int) bool

func $infix<=(o int) bool

func $infix>(o int) bool

func $infix>=(o int) bool

func $infix+(o int) int

func $infix-(o int) int

func $infix*(o int) int

func $infix/(o int) int

func $infix%(o int) int

func $infix^(o int) int

func $infix&(o int) int

func $infix|(o int) int

func $infix||(o int) int

func $infix<<(o int) int

func $infix>>(o int) int

func fmt(kind symbol) String

Formats the integer using a kind specifier which can be binary .b, octal .o, decimal .d, hexadecimal .x, ASCII .c.

func fmt(kind symbol, config Table) String

opts.pad provides the ASCII rune that is used for padding with a string length of config.width.

func int.$call(val any) int

Converts a value to an 48-bit integer.

^topic

type float #

func $prefix-() float

func $infix<(o float) bool

func $infix<=(o float) bool

func $infix>(o float) bool

func $infix>=(o float) bool

func $infix+(o float) float

func $infix-(o float) float

func $infix*(o float) float

func $infix/(o float) float

func $infix%(o float) float

func $infix^(o float) float

func float.$call(val any) float

Converts the value to a float. Panics if type conversion fails.

^topic

type placeholder1 #

^topic

type placeholder2 #

^topic

type placeholder3 #

^topic

type dynamic #

^topic

type any #

^topic

type type #

^topic

type List #

func $index(idx int) dynamic

func $index(range Range) List

func $setIndex(idx int, val any)

func append(val any)

Appends a value to the end of the list.

func appendAll(list List)

Appends the elements of another list to the end of this list.

func insert(idx int, val any)

Inserts a value at index idx.

func iterator() ListIterator

Returns a new iterator over the list elements.

func join(sep String) String

Returns a new string that joins the elements with separator.

func len() int

Returns the number of elements in the list.

func remove(idx int)

Removes an element at index idx.

func resize(size int)

Resizes the list to len elements. If the new size is bigger, none values are appended to the list. If the new size is smaller, elements at the end of the list are removed.

func sort(lessFn any)

Sorts the list with the given less function. If element a should be ordered before b, the function should return true otherwise false.

func List.fill(val any, n int) List

Creates a list with initial capacity of n and values set to val. If the value is an object, it is shallow copied n times.

^topic

type ListIterator #

func next() ?any

^topic

type Tuple #

func $index(idx int) any

^topic

type Table #

func $initPair(key String, value any)

func $get(name String) dynamic

func $set(name String, value any)

func $index(key any) dynamic

func $setIndex(key any, value any)

^topic

type Map #

func $initPair(key any, value any)

func $index(key any) any

func $setIndex(key any, val any)

func contains(key any) bool

Returns whether there is a value mapped to key.

func get(key any) ?any

Returns value mapped to key or returns none.

func remove(key any) bool

Removes the element with the given key key.

func size() int

Returns the number of key-value pairs in the map.

func iterator() MapIterator

Returns a new iterator over the map elements.

^topic

type MapIterator #

func next() ?any

^topic

type String #

func $infix+(o any) String

Returns a new string that concats this string and str.

func concat(o String) String

Returns a new string that concats this string and str.

func count() int

Returns the number of runes in the string.

func endsWith(suffix String) bool

Returns whether the string ends with suffix.

func find(needle String) ?int

Returns the first byte index of substring needle in the string or none if not found. SIMD enabled.

func findAnyRune(runes String) ?int

Returns the first byte index of any rune in runes or none if not found. SIMD enabled.

func findRune(rune int) ?int

Returns the first byte index of a rune needle in the string or none if not found. SIMD enabled.

func insert(idx int, str String) String

Returns a new string with str inserted at byte index idx.

func isAscii() bool

Returns whether the string contains all ASCII runes.

func len() int

Returns the byte length of the string. See count() to obtain the number of runes.

func less(other String) bool

Returns whether this string is lexicographically before other.

func lower() String

Returns this string in lowercase.

func replace(needle String, replacement String) String

Returns a new string with all occurrences of needle replaced with replacement.

func repeat(n int) String

Returns a new string with this string repeated n times.

func seek(idx int) int

Returns the starting byte index for the rune index idx.

func sliceAt(idx int) String

Returns the UTF-8 rune starting at byte index idx as a string.

func $index(idx int) String

Returns the rune at byte index idx. The replacement character (0xFFFD) is returned for an invalid UTF-8 rune.

func $index(range Range) String

Returns a slice into this string from a Range with start (inclusive) to end (exclusive) byte indexes.

func split(sep String) List

Returns a list of UTF-8 strings split at occurrences of sep.

func startsWith(prefix String) bool

Returns whether the string starts with prefix.

func trim(mode symbol, delims String) String

Returns the string with ends trimmed from runes in delims. mode can be .left, .right, or .ends.

func upper() String

Returns this string in uppercase.

func String.$call(val any) String

Converts a value to a string.

^topic

type Array #

func $infix+(o any) Array

func concat(other Array) Array

Returns a new array that concats this array and other.

func decode() String

Calls decode(.utf8)

func decode(encoding symbol) String

Decodes the array based on an encoding. Supported encodings: .utf8. Returns the decoded string or throws error.Decode.

func endsWith(suffix Array) bool

Returns whether the array ends with suffix.

func find(needle Array) ?int

Returns the first index of needle in the array or none if not found.

func findAnyByte(bytes Array) ?int

Returns the first index of any bytes in arrays or none if not found.

func findByte(byte int) ?int

Returns the first index of byte in the array or none if not found.

func fmt(kind symbol) String

Formats each byte in the array using a kind specifier which can be binary .b, octal .o, decimal .d, hexadecimal .x, ASCII .c. Each byte is zero padded.

func getByte(idx int) int

Returns the byte value (0-255) at the given index idx.

func getInt(idx int, endian symbol) int

Returns the int value of the 6 bytes starting from idx with the given endianness (.little or .big).

func getInt32(idx int, endian symbol) int

Returns the int value of the 4 bytes starting from idx with the given endianness (.little or .big).

func insert(idx int, arr Array) Array

Returns a new array with arr inserted at index idx.

func insertByte(idx int, byte int) Array

Returns a new array with byte inserted at index idx.

func iterator() ArrayIterator

Returns a new iterator over the array bytes.

func len() int

Returns the number of bytes in the array.

func repeat(n int) Array

Returns a new array with this array repeated n times.

func replace(needle Array, replacement Array) Array

Returns a new array with all occurrences of needle replaced with replacement.

func $index(idx int) int

func $index(range Range) Array

Returns a slice into this array from a Range with start (inclusive) to end (exclusive) indexes.

func split(sep Array) List

Returns a list of arrays split at occurrences of sep.

func startsWith(prefix Array) bool

Returns whether the array starts with prefix.

func trim(mode symbol, delims Array) Array

Returns the array with ends trimmed from runes in delims. mode can be .left, .right, or .ends.

func Array.$call(val any) Array

Converts a string to an byte Array.

^topic

type ArrayIterator #

func next() ?any

^topic

type pointer #

func addr() int

Returns the memory address as an int. The value may be negative since it's bitcasted from an unsigned 48-bit integer but it retains the original pointer bits.

func asObject() any

Unsafe. Casts the pointer to a Cyber object. The object is retained before it's returned.

func fromCstr(offset int) Array

Unsafe. Returns an Array from a null terminated C string.

func get(offset int, ctype symbol) any

Unsafe. Dereferences the pointer at a byte offset and returns the C value converted to Cyber.

func set(offset int, ctype symbol, val any)

Unsafe. Converts the value to a compatible C value and writes it to a byte offset from this pointer.

func toArray(offset int, len int) Array

Unsafe. Returns an Array with a copy of the byte data starting from an offset to the specified length.

func pointer.$call(val any) pointer

Converts a int to a pointer value, or casts to a pointer. This is usually used with FFI.

^topic

type Closure #

^topic

type Lambda #

^topic

type HostFunc #

^topic

type ExternFunc #

func addr() int

Returns the memory address as an int. The value may be negative since it's bitcasted from an unsigned 48-bit integer but it retains the original pointer bits.

^topic

type Fiber #

func status() symbol

^topic

type metatype #

func id() int

^topic

type Range #

^topic

type Box #

^topic

type TccState #

^topic

module cy #

The cy module contains functions related to the Cyber language.

Sample usage:

use cy
print cy.toCyon([1, 2, 3])

^topic

func eval(src String) any

Evaluates source code in an isolated VM. If the last statement is an expression, a primitive or a String can be returned.

func parse(src String) Map

Parses Cyber source string into a structured map object. Currently, only metadata about static declarations is made available but this will be extended to include an AST.

func parseCyon(src String) any

Parses a CYON string into a value.

func repl(read_line any)

Starts an isolated REPL session. The callback read_line(prefix String) String is responsible for obtaining the input.

func toCyon(val any) String

Encodes a value to CYON string.

^topic

module math #

The math module contains commonly used math constants and functions.

Sample usage:

use math

var r = 10.0
print(math.pi * r^2)

^topic

var e float

Euler's number and the base of natural logarithms; approximately 2.718.

var inf float

Infinity.

var log10e float

Base-10 logarithm of E; approximately 0.434.

var log2e float

Base-2 logarithm of E; approximately 1.443.

var ln10 float

Natural logarithm of 10; approximately 2.303.

var ln2 float

Natural logarithm of 2; approximately 0.693.

var maxSafeInt float

The maximum integer value that can be safely represented as a float. 2^53-1 or 9007199254740991.

var minSafeInt float

The minumum integer value that can be safely represented as a float. -(2^53-1) or -9007199254740991.

var nan float

Not a number. Note that nan == nan. However, if a nan came from an arithmetic operation, the comparison is undefined. Use isNaN instead.

var neginf float

Negative infinity.

var pi float

Ratio of a circle's circumference to its diameter; approximately 3.14159.

var sqrt1_2 float

Square root of ½; approximately 0.707.

var sqrt2 float

Square root of 2; approximately 1.414.

func abs(a float) float

Returns the absolute value of x.

func acos(a float) float

Returns the arccosine of x.

func acosh(a float) float

Returns the hyperbolic arccosine of x.

func asin(a float) float

Returns the arcsine of x.

func asinh(a float) float

Returns the hyperbolic arcsine of a number.

func atan(a float) float

Returns the arctangent of x.

func atan2(a float, b float) float

Returns the arctangent of the quotient of its arguments.

func atanh(a float) float

Returns the hyperbolic arctangent of x.

func cbrt(a float) float

Returns the cube root of x.

func ceil(a float) float

Returns the smallest integer greater than or equal to x.

func clz32(a float) float

Returns the number of leading zero bits of the 32-bit integer x.

func cos(a float) float

Returns the cosine of x.

func cosh(a float) float

Returns the hyperbolic cosine of x.

func exp(a float) float

Returns e^x, where x is the argument, and e is Euler's number (2.718…, the base of the natural logarithm).

func expm1(a float) float

Returns subtracting 1 from exp(x).

func floor(a float) float

Returns the largest integer less than or equal to x.

func frac(a float) float

Returns the fractional or decimal part of a float value.

func hypot(a float, b float) float

Returns the square root of the sum of squares of its arguments.

func isInt(a float) bool

Returns true if the float has no fractional part, otherwise false.

func isNaN(a float) bool

Returns whether x is not a number.

func ln(a float) float

Returns the natural logarithm (㏒e; also, ㏑) of x.

func log(a float, b float) float

Returns the logarithm of y with base x.

func log10(a float) float

Returns the base-10 logarithm of x.

func log1p(a float) float

Returns the natural logarithm (㏒e; also ㏑) of 1 + x for the number x.

func log2(a float) float

Returns the base-2 logarithm of x.

func max(a float, b float) float

Returns the largest of two numbers.

func min(a float, b float) float

Returns the smallest of two numbers.

func mul32(a float, b float) float

Returns the result of the 32-bit integer multiplication of x and y. Integer overflow is allowed.

func pow(a float, b float) float

Returns base x to the exponent power y (that is, x^y).

func random() float

Returns a pseudo-random number between 0 and 1.

func round(a float) float

Returns the value of the number x rounded to the nearest integer.

func sign(a float) float

Returns the sign of the x, indicating whether x is positive, negative, or zero.

func sin(a float) float

Returns the sine of x.

func sinh(a float) float

Returns the hyperbolic sine of x.

func sqrt(a float) float

Returns the positive square root of x.

func tan(a float) float

Returns the tangent of x.

func tanh(a float) float

Returns the hyperbolic tangent of x.

func trunc(a float) float

Returns the integer portion of x, removing any fractional digits.

^topic

Std modules. #

Std modules come with Cyber's CLI. They include:

^topic

module cli #

The cli module contains functions related to the command line.

Sample usage:

use cli
cli.repl()

^topic

func repl()

Starts an isolated REPL session. Invokes cy.repl(replReadLine).

func replReadLine(prefix String) String

Default implementation to read a line from the CLI for a REPL.

^topic

module os #

Cyber's os module contains system level functions. It's still undecided as to how much should be included here so it's incomplete. You can still access os and libc functions yourself using Cyber's FFI or embedding API.

Sample usage:

use os

var map = os.getEnvAll()
for map -> [k, v]:
    print "$(k) -> $(v)"

^topic

var cpu String

The current cpu arch's tag name.

var endian symbol

The current arch's endianness: .little, .big

var stderr File

Standard error file descriptor.

var stdin File

Standard input file descriptor.

var stdout File

Standard output file descriptor.

var system String

The current operating system's tag name.

var vecBitSize int

Default SIMD vector bit size.

func access(path String, mode symbol)

Attempts to access a file at the given path with the .read, .write, or .readWrite mode. Throws an error if unsuccessful.

func args() List

Returns the command line arguments in a List. Each argument is converted to a String.

func cacheUrl(url String) String

Returns the path of a locally cached file of url. If no such file exists locally, it's fetched from url.

func copyFile(srcPath String, dstPath String)

Copies a file to a destination path.

func createDir(path String)

Creates the directory at path. Returns true if successful.

func createFile(path String, truncate bool) File

Creates and opens the file at path. If truncate is true, an existing file will be truncated.

func cstr(s any) pointer

Returns a null terminated C string.

func cwd() String

Returns the current working directory.

func dirName(path String) ?String

Returns the given path with its last component removed.

func execCmd(args List) Map

Runs a shell command and returns the stdout/stderr.

func exePath() String

Returns the current executable's path.

func exit(status int)

Exits the program with a status code.

func fetchUrl(url String) Array

Fetches the contents at url using the HTTP GET request method.

func free(ptr pointer)

Frees the memory located at ptr.

func getEnv(key String) ?String

Returns an environment variable by key.

func getEnvAll() Map

Returns all environment variables as a Map.

func malloc(size int) pointer

Allocates size bytes of memory and returns a pointer.

func milliTime() float

Return the calendar timestamp, in milliseconds, relative to UTC 1970-01-01. For an high resolution timestamp, use now().

func newFFI() FFI

Returns a new FFI context for declaring C mappings and binding a dynamic library.

func now() float

Returns the current time (in high resolution seconds) since an arbitrary point in time.

func openDir(path String) Dir

Invokes openDir(path, false).

func openDir(path String, iterable bool) Dir

Opens a directory at the given path. iterable indicates that the directory's entries can be iterated.

func openFile(path String, mode symbol) File

Opens a file at the given path with the .read, .write, or .readWrite mode.

func parseArgs(options List) Table

Given expected ArgOptions, returns a Table of the options and a rest entry which contains the non-option arguments.

func readAll() String

Reads stdin to the EOF as a UTF-8 string. To return the bytes instead, use stdin.readAll().

func readFile(path String) String

Reads the file contents from path as a UTF-8 string. To return the bytes instead, use File.readAll().

func readLine() String

Reads stdin until a new line as a String. This is intended to read user input from the command line. For bulk reads from stdin, use stdin.

func realPath(path String) String

Returns the absolute path of the given path.

func removeDir(path String)

Removes an empty directory at path. Returns true if successful.

func removeFile(path String)

Removes the file at path. Returns true if successful.

func setEnv(key String, val String)

Sets an environment variable by key.

func sleep(ms float)

Pauses the current thread for given milliseconds.

func unsetEnv(key String)

Removes an environment variable by key.

func writeFile(path String, contents any)

Writes contents as a string or bytes to a file.

^topic

type File #

func close()

Closes the file handle. File ops invoked afterwards will return error.Closed.

func iterator() any

func next() any

func read(n int) Array

Reads at most n bytes as an Array. n must be at least 1. A result with length 0 indicates the end of file was reached.

func readAll() Array

Reads to the end of the file and returns the content as an Array.

func seek(n int)

Seeks the read/write position to pos bytes from the start. Negative pos is invalid.

func seekFromCur(n int)

Seeks the read/write position by pos bytes from the current position.

func seekFromEnd(n int)

Seeks the read/write position by pos bytes from the end. Positive pos is invalid.

func stat() Map

Returns info about the file as a Map.

func streamLines() File

Equivalent to streamLines(4096).

func streamLines(bufSize int) File

Returns an iterable that streams lines ending in \n, \r, \r\n, or the EOF. The lines returned include the new line character(s). A buffer size of bufSize bytes is allocated for reading. If \r is found at the end of the read buffer, the line is returned instead of waiting to see if the next read has a connecting \n.

func write(val any) int

Writes a String or Array at the current file position. The number of bytes written is returned.

^topic

type Dir #

func iterator() DirIterator

Returns a new iterator over the directory entries. If this directory was not opened with the iterable flag, error.NotAllowed is returned instead.

func stat() Map

Returns info about the file as a Map.

func walk() DirIterator

Returns a new iterator over the directory recursive entries. If this directory was not opened with the iterable flag, error.NotAllowed is returned instead.

^topic

type DirIterator #

func next() ?Map

^topic

type FFI #

func bindCallback(fn any, params List, ret symbol) ExternFunc

Creates an ExternFunc that contains a C function pointer with the given signature. The extern function is a wrapper that calls the provided user function. Once created, the extern function is retained and managed by the FFI context.

func bindLib(path ?String) any

Calls bindLib(path, [:]).

func bindLib(path ?String, config Table) any

Creates a handle to a dynamic library and functions declared from cfunc. By default, an anonymous object is returned with the C-functions binded as the object's methods. If config contains gen_table: true, a Table is returned instead with C-functions binded as function values.

func bindObjPtr(obj any) pointer

Returns a Cyber object's pointer. Operations on the pointer is unsafe, but it can be useful when passing it to C as an opaque pointer. The object is also retained and managed by the FFI context.

func cbind(mt metatype, fields List)

Binds a Cyber type to a C struct.

func cfunc(name String, params List, ret any)

Declares a C function which will get binded to the library handle created from bindLib.

func new(ctype symbol) pointer

Allocates memory for a C struct or primitive with the given C type specifier. A pointer to the allocated memory is returned. Eventually this will return a cpointer instead which will be more idiomatic to use.

func unbindObjPtr(obj any)

Releases the object from the FFI context. External code should no longer use the object's pointer since it's not guaranteed to exist or point to the correct object.

^topic

type CArray #

^topic

type CDimArray #

^topic

Map DirEntry #

keysummary
'name' -> ArrayThe name of the file or directory.
'type' -> #file | #dir | #unknownThe type of the entry.
^topic

Map DirWalkEntry #

keysummary
'name' -> ArrayThe name of the file or directory.
'path' -> ArrayThe path of the file or directory relative to the walker's root directory.
'type' -> #file | #dir | #unknownThe type of the entry.
^topic

Table ArgOption #

keysummary
'name' -> StringThe name of the option to match excluding the hyphen prefix. eg. -path
'type' -> metatype(String | float | boolean)Parse as given value type.
'default' -> anyOptional: Default value if option is missing. none is used if this is not provided.
^topic

module test #

The test module contains utilities for testing.

Sample usage:

use t 'test'

var a = 123 + 321
t.eq(a, 444)

^topic

func assert(pred bool)

Panics if pred is false.

func eq(a any, b any) bool

Returns whether two values are equal. Panics with error.AssertError if types or values do not match up.

func eqList(a any, b any) bool

Returns true if two lists have the same size and the elements are equal as if eq was called on those corresponding elements.

func eqNear(a any, b any) bool

Returns true if two numbers are near each other within epsilon 1e-5.

func fail()

func fail(msg String)

func throws(fn any, err error)

Asserts that an error was thrown when invoking a function.

^topic

FFI. #

^top

Cyber supports binding to an existing C ABI compatible library at runtime. This allows you to call into dynamic libraries created in C or other languages. Cyber uses libtcc to JIT compile the bindings so function calls are fast. The example shown below can be found in Examples.

FFI context. #

An FFI context contains declarations that map C to Cyber. Afterwards, it allows you to bind to a dynamic library or create interoperable objects. To create a new FFI context:

use os

var ffi = os.newFFI()
^topic

Declare functions. #

Functions from a library are first declared using cfunc which accepts C types in the form of symbols. In a future update they will accept C syntax instead.

ffi.cfunc('add', [.int, .int], .int)

The first argument refers to the symbol's name in the dynamic library. The second argument contains the function's parameter types and finally the last argument is the function's return type.

The example above maps to this C function:

int add(int a, int b) {
    return a + b;
}
^topic

Bind library. #

bindLib accepts the path to the library and returns a object which can be used to invoke the functions declared from cfunc:

let lib = ffi.bindLib('./mylib.so')
lib.add(123, 321)

Note that let is used to allow lib to be used dynamically since the type is unknown at compile-time.

^topic

Search path. #

If the path argument to bindLib is just a filename, the search steps for the library is specific to the operating system. Provide an absolute (eg. '/foo/mylib.so') or relative (eg. './mylib.so') path to load from a direct location instead. When the path argument is none, it loads the currently running executable as a library allowing you to bind exported functions from the Cyber CLI or your own application/runtime.

^topic

Configuration. #

By default bindLib returns an anonymous object with the binded C-functions as methods. This is convenient for invoking functions using the method call syntax. If a config is passed into bindLib as the second argument, gen_table: true makes bindLib return a table instead with the binded C-functions as Cyber functions.

^topic

Finalizer. #

The resulting object of bindLib holds a reference to an internal TCCState which owns the loaded JIT code. Once the object is released by ARC, the TCCState is also released which removes the JIT code from memory.

^topic

Mappings. #

When using cfunc or cbind declarations, symbols are used to represent default type mappings from Cyber to C and back:

Incomplete: This is not the final API for dynamically loading and interfacing with C libraries. The plan is to parse a subset of C headers to bind to Cyber types and functions.

BindingCyberC
.boolboolbool
.charintint8_t, signed char
.ucharintuint8_t, unsigned char
.shortintint16_t, short
.ushortintuint16_t, unsigned short
.intintint32_t, int
.uintintuint32_t, unsigned int
.longintint64_t, long long
.ulongintuint64_t, unsigned long long
.usizeintsize_t, uintptr_t
.floatfloatfloat
.doublefloatdouble
(1) .charPtrpointerchar*
.voidPtrpointervoid*
(2) type {S}type {S}struct
  1. Use os.cstr() and pointer.fromCstr() to convert between a Cyber string and a null terminated C string.
  2. The mapping from a Cyber object type S and the C-struct can be declared with cbind.
^topic

Bind to Cyber type. #

cbind is used to bind a C struct to a Cyber object type. Once declared, the Cyber type can be used as a binding type in function declarations:

use os

type MyObject:
    a float
    b pointer
    c bool

ffi.cbind(MyObject, [.float, .voidPtr, .bool])
ffi.cfunc('foo', [MyObject], MyObject)
let lib = ffi.bindLib('./mylib.so')

var res = lib.foo(MyObject{a: 123.0, b: os.cstr('foo'), c: true})

The example above maps to these C declarations in mylib.so:

typedef struct MyObject {
    double a;
    char* b;
    bool c;
} MyObject;

MyObject foo(MyObject o) {
    // Do something.
}

cbind also generates ptrTo[Type] as a helper function to dereference an opaque ptr to a new Cyber object:

ffi.cfunc('foo', [MyObject], .voidPtr)
let lib = ffi.bindLib('./mylib.so')

var ptr = lib.foo(MyObject{a: 123, b: os.cstr('foo'), c: true})
var res = lib.ptrToMyObject(ptr)
^topic

Pointers. #

A pointer is used to read or write to an exact memory address. This is typically used for FFI to manually map Cyber types to C, and back. See type pointer.

A new pointer can be created with the builtin pointer.

var ptr = pointer(0xDEADBEEF)
print ptr.value()     --'3735928559'
^topic

cbindgen.cy #

cbindgen.cy is a Cyber script that automatically generates bindings given a C header file. Some example bindings that were generated include: Raylib and LLVM.

^topic

Error Handling. #

^top

Cyber provides a throw/catch mechanism to handle expected errors. For unexpected errors, panics can be used as a fail-fast mechanism to abort the currently running fiber.

Error value. #

^topic

error literal. #

An error value contains a symbol. They can be created without a declaration using the error literal:

var err = error.Oops

Use sym() to obtain the underlying symbol:

print err.sym()   -- Prints ".Oops"

Since error is a primitive value, it can be compared using the == operator.

if err == error.Oops:
    handleOops()

-- Alternatively.
if err.sym() == .Oops:
    handleOops()
^topic

error payload. #

An payload value can be attached when throwing an error value. Planned Feature

^topic

error set type. #

Error set types enumerate error values that belong to the same group of errors: Planned Feature

type MyError error:
    case boom
    case badArgument
    case nameTooLong

var err = MyError.nameTooLong
^topic

Throwing errors. #

Use the throw keyword to throw errors. A thrown error continues to bubble up the call stack until it is caught by a try block or try expression.

func fail():
    throw error.Oops      -- Throws an error with the symbol `#Oops`

func fail2():
    throw 123             -- panic: Can only throw an `error` value.

throw can also be used as an expression.

func fail():
    var a = false or throw error.False
^topic

Catching errors. #

^topic

try block. #

The try block catches thrown errors and resumes execution in a followup catch block:

try:
    funcThatCanFail()
catch err:
    print err      -- 'error.Failed'
^topic

caught variable. #

The contextual caught variable is used to reference the caught error: Planned Feature

try:
    funcThatCanFail()
catch:
    print caught   -- 'error.Failed'
^topic

catch matching. #

An inner catch block contains a matching clause: Planned Feature

try:
    funcThatCanFail()
catch error.BadDay:
    eatSnack()
catch:
    print caught

Error sets can be matched: Planned Feature

try:
    funcThatCanFail()
catch MyError.Boom:
    print 'Kaboom!'
catch:
    print caught
^topic

try expression. #

The try expression either returns a non-error result or the default value from the catch clause:

var res = try funcThatCanFail() catch 123
print res         -- '123'

Since errors bubble up automatically, any errors thrown from sub-expressions are also caught:

var res = try happyFunc(funcThatCanFail()) catch 123
print res         -- '123'
^topic

Value or error. #

When the catch clause is omitted, the try expression will return either the value or the error:

var res = try funcThatCanFail()
if res == error.Failed:
    print 'Result is an error.'
^topic

Semantic checks. #

^topic

throws specifier. #

The throws specifier ! indicates that a function contains a throwing expression that was not caught with try catch.

The specifier is attached to the function return type as a prefix:

func foo() !void:
    throw error.Failure

This declaration indicates the function can either return an int type or throw an error:

func result(cond bool) !int:
    if cond:
        return 123
    else:
        throw error.Failure
^topic

throws check. #

The compiler requires a throws specifier if the function contains an uncaught throwing expression: Planned Feature

func foo(a int) int:
    if a == 10:
        throw error.Failure   --> CompileError.
    else:
        return a * 2

--> CompileError. Uncaught throwing expression.
--> `foo` requires the `!` throws specifier or
--> the expression must be caught with `try`.
^topic

Stack trace. #

When an uncaught error bubbles up to the top, its stack trace from the throw callsite is dumped to the console. The builtin errorTrace() and errorReport() are used to obtain the stack trace info.

try:
    funcThatCanFail()
catch:
    -- Prints the stack trace summary of the caught error.
    print errorReport()

    -- Provides structured info about the stack trace.
    var info = errorTrace()
    print info.frames.len()
^topic

Unexpected errors. #

An unexpected error is an error that is not meant to be handled at runtime.

^topic

Panics. #

The builtin panic is used as a fail-fast mechanism to quickly exit the current fiber with an error payload:

func kaboom():
    panic(error.danger)

kaboom()     -- Script ends and prints the stack trace.

Panics can not be caught using try catch. Once panic is invoked, the current fiber stops execution and begins to unwind its call stack. Once the error is propagated to the root, the fiber ends and transitions to a panic state. If the main fiber ends this way, the VM begins to shutdown. Otherwise, execution resumes on the next fiber which allows recovery from a panic.

^topic

Concurrency. #

^top

Cyber supports fibers as a concurrency mechanism. There are plans to support preemptive concurrency with async/await as well as multithreading.

Fibers. #

A fiber represents a separate execution context as a first-class value. It contains it's own call stack and program counter. Fibers by themselves do not enable parallelism.

^topic

Creating fibers. #

The coinit keyword creates and returns a new fiber using a function as the entry point:

var count = 0

var foo = func ():
    count += 1
    coyield
    count += 1

var fiber = coinit(foo)

print count          -- '0'
coresume fiber
print count          -- '1'
coresume fiber
print count          -- '2'

A fiber does not start execution until coresume is invoked on it. coyield pauses the current fiber and execution is returned to the previous fiber that invoked coresume.

^topic

Passing arguments. #

Arguments after the callee are passed into the entry function:

var count = 0

var increment = func (inc):
    count += inc

var fiber = coinit(increment, 5)
coresume fiber
print count          -- '5'

When the fiber is created, the arguments are saved inside the fiber's stack. Once the first coresume is invoked, the entry function is invoked with the saved arguments.

^topic

Reset state. #

To reset a fiber to its initial state, invoke reset(). Planned Feature When reset, the existing stack is unwinded, the program counter returns to the starting point, and the state is set to .init:

func fib(n int) int:
    coyield n
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

var task = coinit(fib, 10)

-- Progress the fiber...
print(coresume task)    -- Prints "10"
print(coresume task)    -- Prints "9"
print(coresume task)    -- Prints "8"

-- Reset back to the start with the `.init` state.
fiber.reset()
print(coresume task)    -- Prints "10"
^topic

Rebinding arguments. #

Arguments attached to the fiber can be rebinded with a different set of values. Planned Feature This allows fiber reuse, instead of creating a new fiber:

var task = coinit(fib, 10)

-- Run task to completion.
var res = 0
while fiber.status() != .done:
    res = coresume fiber
print res

fiber.reset()
fiber.bindArgs(20)

-- Run task again with the new argument...
^topic

Fiber block. #

A fiber block is used to construct a fiber without an entry function. Planned Feature The counting example can be rewritten to:

var count = 0

var fiber = coinit:
    count += 1       -- `count is captured`
    coyield
    count += 1

print count          -- '0'
coresume fiber
print count          -- '1'
coresume fiber
print count          -- '2'

Referencing parent variables from the fiber block automatically captures them just like a function closure.

^topic

Pause and resume. #

coyield can be used anywhere in a fiber's call stack to pause execution and return to the previous fiber.

func foo():
    print 'foo'
    bar()

func bar():
    -- Nested coyield in call stack.
    coyield
    print 'bar'

var fiber = coinit(foo)
coresume fiber

coresume also returns the resulting value.

func foo():
    return 123

var fiber = coinit(foo)
print(coresume fiber)    -- '123'

coyield can return a value back to coresume. Planned Feature

^topic

Fiber state. #

Use Fiber.status() to get the current state of the fiber.

func foo():
    coyield
    print 'done'

var fiber = coinit(foo)
print fiber.status()   -- '.paused'
coresume fiber
print fiber.status()   -- '.paused'
coresume fiber
print fiber.status()   -- '.done'

The main execution context is a fiber as well. Once the main fiber has finished, the VM is done and control is returned to the host.

^topic

Gas mileage. #

Planned Feature

^topic

Async. #

Planned Feature

^topic

Multi-thread. #

Planned Feature

^topic

Dynamic Typing. #

^top

Dynamic typing is supported with a less restrictive syntax. This can reduce the amount of friction when writing code, but it can also result in more runtime errors.

In Cyber, the let keyword is used exclusively for dynamic declarations.

Dynamic variables. #

Variables declared with let are implicitly given the dynamic type:

let a = 123

Typically a dynamic variable defers type checking to runtime, but if the compiler determines that an operation will always fail at runtime, a compile error is reported instead:

let a = '100'

print a / 2
--> CompileError: Can not find the symbol `$infix/` in `String`

When a is assigned a different type of value, its recent type is updated so the compiler can continue to surface errors ahead of time:

a = [1, 2, 3]
print a / 2
--> CompileError: Can not find the symbol `$infix/` in `List`
^topic

Runtime type checking. #

If the type of a dynamic variable can not be determined at compile-time, type checking is deferred to runtime.

In this example, the type for a is unknown after assigning the return of a dynamic call to erase. Any operation on a would defer type checking to runtime:

let a = erase(123)

print a(1, 2, 3)
--> panic: Expected a function.

If a dynamic variable's recent type differs between two branches of execution, the type is considered unknown after the branches are merged. Any operations on the variable afterwards will defer type checking to runtime:

let a = 123
if a > 20:
    a = 'hello'

-- Branches are merged. `a` has an unknown type.

print a(1, 2, 3)
--> panic: Expected a function.
^topic

Dynamic functions. #

Functions declared with let do not allow typed parameters or a return specifier. All parameters are implicitly given the dynamic type.

The return specifier is also implicitly !dynamic which indicates that the function can throw an error. This is only relevant when typed code calls a dynamic function:

let foo(a, b, c):
    return a + b() + a[c]

The function foo is a static function. It can't be reassigned like a variable after its declaration.

However, function values (lambdas) assigned to variables allow reassignment. Lambdas can also capture variables from the parent scope:

let count = 123
let f = (a, b):
    return a + b(10) + count

-- Reassign `f`.
f = (a, b):
    return count * 2 - b(a)

Lambdas that simply return an expression can be written as:

f = (a, b) => a + b(10)
^topic

Dynamic tables. #

The builtin Table type is used to create dynamic objects. Tables are initialized with the record literal:

let a = {}
a.name = 'Nova'
print a.name     --> Nova

Read more about how to use Tables.

^topic

Custom tables. #

Custom tables allow declaring fields and methods.

let Counter{ count }:
    let inc():
        count += 1
        
let c = Counter{ count: 0 }
c.inc()
print c.count       --> 1

Fields are declared inside the braces and separated with commas. Unlike typed declarations, the fields declared only serve as a subset constraint. Additional fields can still be initialized and used.

Tables can be declared without any methods:

let Vec3{x, y, z}

let v = Vec3{x: 1, y: 2, z: 3}
^topic

Metaprogramming. #

^top

Operator overloading. #

All operators are implemented as object methods.

Incomplete: Not all operators have transitioned to the method paradigm.

To overload an operator for an object type, declare $prefix, $infix, $postfix methods. See the available builtin operators. Since operator names aren't allowed as standard identifiers, they are contained in a string literal.

type Vec2:
    x float
    y float

    func '$infix+'(o Vec2) Vec2:
        return Vec2{
            x: x + o.x,
            y: y + o.y,
        }

    func '$prefix-'() Vec2:
        return Vec2{x: -x, y: -y}

var a = Vec2{x: 1, y: 2}
var b = a + Vec2{x: 3, y: 4}
var c = -a

Some special operators have their own name. This example overloads the index operator and the set index operator:

type MyCollection:
    arr List

    func $index(idx):
        return arr[idx * 2]

    func $setIndex(idx, val):
        arr[idx * 2] = val 

var a = MyCollection{arr: [1, 2, 3, 4]}
print a[1]        -- Prints `3`
^topic

Builtin operators. #

A list of all supported operators:

OperatorName
Bitwise not$prefix~
Minus$prefix-
Greater$infix>
Greater equal$infix>=
Less$infix<
Less equal$infix<=
Add$infix+
Subtract$infix-
Multiply$infix*
Divide$infix/
Modulus$infix%
Power$infix^
Bitwise and$infix&
Bitwise or$infix|
Bitwise xor$infix||
Bitwise left shift$infix<<
Bitwise right shift$infix>>
Index$index
Set index$setIndex
Slice$slice
^topic

Custom operators. #

Planned Feature

^topic

Magic functions. #

^topic

Call module. #

Declare a $call function to allow invoking a module as a function.

-- Object types are also modules.
type Vec2:
    x float
    y float

func Vec2.$call(x float, y float) Vec2:
    return Vec2{x: x, y: y}

var v = Vec2(1, 2)
^topic

$initRecord method. #

Planned Feature

^topic

$initPair method. #

The $initPair method overrides the record initializer. After an instance of the type is created from its default record initializer, this method is invoked for each key-value pair in the record literal:

type MyMap:
    func $initPair(key any, value any) void:
        print "$(key): $(value)"

var m = MyMap{ a: 123, b: 234 }
--> a: 123
--> b: 234

$initPair is only allowed if the type has a default record initializer or $initRecord is declared.

^topic

$get method. #

The $get method allows overriding field accesses for undeclared fields:

type Foo:
    func $get(name String):
        return name.len()

var f = Foo{}
print f.abc      --> 3
print f.hello    --> 5
^topic

$set method. #

The $set method allows overriding field assignments for undeclared fields:

type Foo:
    func $set(name String, value any):
        print "setting $(name) $(value)"

var f = Foo{}
f.abc = 123      --> setting abc 123
^topic

Missing method. #

Declare a $missing method as a fallback when a method was not found in an instance.

Planned Feature

type A:

    func $missing(args...):
        return args.len

var a = A{}
print a.foo()      -- Output: '0'
print a.bar(1, 2)  -- Output: '2'
^topic

Reflection. #

A type metatype object references an internal type. Use the typeof builtin to get the metatype of a value.

var val = 123
print typeof(val)   -- 'type: float'

-- Referencing a type as a value also returns its `metatype`.
print bool          -- 'type: bool'
^topic

Directives. #

Directives start with # and are used as modifiers.

#host

Modifier to bind a function, variable, or type to the host. See libcyber.

^topic

Generics. #

Generics enables parametric polymorphism for types and functions. Compile-time arguments are passed to templates to generate specialized code. This facilitates developing container types and algorithms that operate on different types.

See Custom Types / Generic types and Functions / Generic functions.

^topic

Macros. #

Planned Feature

^topic

Compile-time execution. #

Planned Feature

^topic

Builtin types. #

Builtin types are used internally by the compiler to define it's own primitive types such as bool, int, and float.

type bool_t

type int64_t

type float64_t

^topic

Builtin functions. #

func genLabel(name String)

Emits a label during codegen for debugging.

^topic

Builtin constants. #

var modUri String

Evaluates to the module's URI as a string. See Module URI.

^topic

Runtime execution. #

cy.eval evaluates source code in an isolated VM. If the last statement is an expression, a primitive or String can be returned to the caller:

use cy

var res = cy.eval('1 + 2')
print res        --> 3
^topic

libcyber. #

^top

libcyber allows embedding the Cyber compiler and VM into applications. Cyber's core types and the CLI app were built using the library.

The API is defined in the C header file. The examples shown below can be found in the repository under c-embedded. C is used as the host language, but it can be easily translated to C++ or any C-ABI compatible language.

Types and constants from the C-API begin with CL and functions begin with cl.

Getting started. #

^topic

Create VM. #

Most operations are tied to a VM handle. To create a new VM instance, call clCreate:

#include "cyber.h"

int main() {
    CLVM* vm = clCreate();
    // ...
    clDestroy(vm);
    return 0;
}
^topic

Override print. #

The builtin print function does nothing by default, so it needs to be overrided to print to stdout for example:

void printer(CLVM* vm, CLStr str) {
    printf("Invoked printer: %.*s\n", (int)str.len, str.buf);
}

int main() {
    // ...
    clSetPrinter(vm, printer);
    // ...
}

Note that print invokes the printer twice, once for the value's string and another for the new line character.

^topic

Eval script. #

clEval compiles and evaluates a script:

CLStr src = STR(
    "var a = 1\n"
    "print(a + 2)\n"
);

CLValue val;
int res = clEval(vm, src, &val);
if (res == CS_SUCCESS) {
    printf("Success!\n");
    clRelease(vm, val);
} else {
    const char* report = clNewLastErrorReport(vm);
    printf("%s\n", report);
    clFreeStrZ(report);
}

If a value is returned from the main block of the script, it's saved to the result value argument. Memory is managed by ARC so a value that points to a heap object requires a clRelease when it's no longer needed.

clEval returns a result code that indicates whether it was successful.

^topic

Module Loader. #

A module loader describes how a module is loaded when use import statement is encountered during script execution. Only one module loader can be active and is set using clSetModuleLoader:

bool modLoader(CLVM* vm, CLStr spec, CLModuleLoaderResult* out) {
    if (strncmp("my_mod", spec.buf, spec.len) == 0) {
        out->src =
            "#host func add(a float, b float) float\n"
            "#host var .MyConstant float\n"
            "#host var .MyList     List\n"
            "\n"
            "#host\n"
            "type MyCollection:\n"
            "    #host func asList() any"
            "\n"
            "#host func MyCollection.new(a, b) MyCollection\n";
        out->funcLoader = funcLoader;
        out->varLoader = varLoader;
        out->typeLoader = typeLoader;
        return true;
    } else {
        // Fallback to the default module loader to load `core`.
        return clDefaultModuleLoader(vm, spec, out);
    }
}

int main() {
    //...
    clSetModuleLoader(vm, modLoader);
    //...
}

The above example checks whether "my_mod" was imported and returns it's source code. Additional loaders are returned to load the functions, variables, and types from the source code.

^topic

Default module loader. #

Since only one module loader can be set to the VM instance, a custom loader is required to handle the "core" import which contains all of the core types and functions in Cyber. This can simply be delegated to clDefaultModuleLoader.

^topic

Function loader. #

A function loader describes how to load a #host function when it's encountered by the compiler. The loader can bind functions and type methods:

struct { char* n; CLFuncFn fn; } funcs[] = {
    {"add", add},
    {"asList", myCollectionAsList},
    {"MyCollection.new", myCollectionNew},
};

bool funcLoader(CLVM* vm, CLFuncInfo info, CLFuncResult* out) {
    // Check that the name matches before setting the function pointer.
    if (strncmp(funcs[info.idx].n, info.name.buf, info.name.len) == 0) {
        out->ptr = funcs[info.idx].fn;
        return true;
    } else {
        return false;
    }
}

This example uses the CLFuncInfo.idx of a #host function to index into an array and return a Host function pointer. The name is also compared to ensure it's binding to the correct pointer.

This is an efficient way to map Cyber functions to host functions. A different implementation might use a hash table to map the name of the function to it's pointer.

^topic

Variable loader. #

A variable loader describes how to load a #host variable when it's encountered by the compiler:

// C has limited static initializers (and objects require a vm instance) so initialize them in `main`.
typedef struct { char* n; CLValue v; } NameValue;
NameValue vars[2];

bool varLoader(CLVM* vm, CLVarInfo info, CLValue* out) {
    // Check that the name matches before setting the value.
    if (strncmp(vars[info.idx].n, info.name.buf, info.name.len) == 0) {
        // Objects are consumed by the module.
        *out = vars[info.idx].v;
        return true;
    } else {
        return false;
    }
}

int main() {
    // ...

    // Initialize var array for loader.
    vars[0] = (NameValue){".MyConstant", clFloat(1.23)};
    CLValue myInt = clInteger(123);
    vars[1] = (NameValue){".MyList", clNewList(vm, &myInt, 1)};

    // ...
}

This example uses the same technique as the function loader, but it can be much simpler. It doesn't matter how the mapping is done as long as the variable loader returns a CLValue.

^topic

Type loader. #

A type loader describes how to load a #host type when it's encountered by the compiler:

CLTypeId myCollectionId;

bool typeLoader(CLVM* vm, CLTypeInfo info, CLTypeResult* out) {
    if (strncmp("MyCollection", info.name.buf, info.name.len) == 0) {
        out->type = CS_TYPE_OBJECT;
        out->data.object.outTypeId = &myCollectionId;
        out->data.object.getChildren = myCollectionGetChildren;
        out->data.object.finalizer = myCollectionFinalizer;
        return true;
    } else {
        return false;
    }
}

When binding to the "MyCollection" type, it's typeId is saved to outTypeId. This id is then used to create new instances of this type. See Host types.

^topic

Host functions. #

A host function requires a specific function signature:

CLValue add(CLVM* vm, const CLValue* args, uint8_t nargs) {
    double res = clAsFloat(args[0]) + clAsFloat(args[1]);
    return clFloat(res);
}

A host function should always return a CLValue. clNone() can be returned if the function does not intend to return any value.

^topic

Host types. #

A host type are types that are opaque to Cyber scripts but still behave like an object. They can have type functions and methods.

Only the host application can directly create new instances of them, so usually a function is binded to expose a constructor to the user script:

// Binding a C struct with it's own children and finalizer.
// This struct retains 2 VM values and has 2 arbitrary data values unrelated to the VM.
typedef struct MyCollection {
    CLValue val1;
    CLValue val2;
    int a;
    double b;
} MyCollection;

// Implement the `new` function in MyCollection.
CLValue myCollectionNew(CLVM* vm, const CLValue* args, uint8_t nargs) {
    // Instantiate our object.
    CLValue new = clNewHostObject(vm, myCollectionId, sizeof(MyCollection));
    MyCollection* my = (MyCollection*)clAsHostObject(new);

    // Assign the constructor args passed in and retain them since the new object now references them.
    clRetain(vm, args[0]);
    my->val1 = args[0];
    clRetain(vm, args[1]);
    my->val2 = args[1];

    // Assign non VM values.
    my->a = 123;
    my->b = 9999.999;
    return new;
}

clNewHostObject takes the type id (returned from the Type loader) and size (in bytes) and returns a new heap object. Note that the size is allowed to vary. Different instances of the same type can occupy different amounts of memory.

^topic

getChildren #

Since MyCollection contains CLValue children, the Type loader requires a getChildren callback so that memory management can reach them:

CLValueSlice myCollectionGetChildren(CLVM* vm, void* obj) {
    MyCollection* my = (MyCollection*)obj;
    return (CLValueSlice){ .ptr = &my->val1, .len = 2 };
}
^topic

finalizer #

A type finalizer is optional since the memory and children of an instance will be freed automatically by ARC. However, it can be useful to perform additional cleanup tasks for instances that contain external resources.

void myCollectionFinalizer(CLVM* vm, void* obj) {
    printf("MyCollection finalizer was called.\n");
}
^topic

Memory. #

^top

Cyber provides memory safety by default.

ARC. #

Cyber uses ARC or automatic reference counting to manage memory. ARC is deterministic and has less overhead compared to a tracing garbage collector. Reference counting distributes memory management, which reduces GC pauses and makes ARC suitable for realtime applications. One common issue in ARC implementations is reference cycles which Cyber addresses with Weak References and it's very own Cycle Detection.

^topic

Reference counting. #

In Cyber, there are primitive and object values. Primitives don't need any memory management, since they are copied by value and no heap allocation is required (with the exception of primitives being captured by a closure.

Objects are managed by ARC. Each object has its own reference counter. Upon creating a new object, it receives a reference count of 1. When the object is copied, it's retained and the reference count increments by 1. When an object value is removed from it's parent or is no longer reachable in the current stack frame, it is released and the reference count decrements by 1.

Once the reference count reaches 0 the object begins its destruction procedure. First, child references are released thereby decrementing their reference counts by 1. If the object is a host object, it will invoke its finalizer function. Afterwards, the object is freed from memory.

^topic

Optimizations. #

The compiler can reduce the number of retain/release ops since it can infer value types even though they are dynamically typed to the user. Arguments passed to functions are only retained depending on the analysis from the callsite.

^topic

Closures. #

When primitive variables are captured by a closure, they are boxed and allocated on the heap. This means they are managed by ARC and cleaned up when there are no more references to them.

^topic

Fibers. #

Fibers are freed by ARC just like any other object. Once there are no references to the fiber, it begins to release it's child references by unwinding it's call stack.

^topic

Heap. #

Many object types in Cyber are small enough to be at or under 40 bytes. To take advantage of this, Cyber can reserve object pools to quickly allocate and free these small objects with very little bookkeeping. Bigger objects are allocated and managed by mimalloc which has proven to be a fast and reliable general-purpose heap allocator.

^topic

Weak references. #

Planned Feature

^topic

Cycle detection. #

The cycle detector is also considered a GC and frees abandoned objects managed by ARC. Although weak references can remove cycles altogether, Cyber does not force you to use them and provides a manual GC as a one-time catch all solution.

Incomplete Feature: Only the main fiber stack is cleaned up at the moment.

To invoke the GC, call the builtin function: performGC.

func foo():
    -- Create a reference cycle.
    var a = []
    var b = []
    a.append(b)
    b.append(a)

    -- Cycle still alive in the current stack so no cleanup is done.
    var res = performGC()
    print res['numCycFreed']    -- Output: 0
    print res['numObjFreed']    -- Output: 0

foo()
-- `a` and `b` are no longer reachable, so the GC does work.
var res = performGC()
print res['numCycFreed']      -- Output: 2
print res['numObjFreed']      -- Output: 2
^topic

CLI. #

^top

REPL. #

The REPL is started by running the CLI without any arguments:

cyber

The REPL starts new sessions with use $global. This allows undeclared variables to be used similar to other dynamic languages:

> a = 123
> a * 2
`int` 246

When the first input ends with :, the REPL will automatically indent the next line. To recede the indentation, provide an empty input. Once the indent returns to the beginning, the entire code block is submitted for evaluation:

> if true:
    | print 'hello!'
    | 
hello!

Top level declarations such as imports, types, and functions can be referenced in subsequent evals:

> use math
> math.random()
`float` 0.3650744641604983
> type Foo:
    | a int
    |
> f = Foo{a: 123}
> f.a
`int` 123

Local variables can not be referenced in subsequent evals, since their scope ends with each eval input:

> var a = 123
> a
panic: Variable is not defined in `$global`.

input:1:1 main:
a
^
^topic

JIT compiler. #

Cyber's just-in-time compiler is incomplete and unstable. To run your script with JIT enabled:

cyber -jit <script>

The goal of the JIT compiler is to be fast at compilation while still being significantly faster than the interpreter. The codegen involves stitching together pregenerated machine code that targets the same runtime stack slots used by the VM. This technique is also known as copy-and-patch. As the VM transitions to unboxed data types, the generated code will see more performance gains.

^topic

AOT compiler. #

The ahead-of-time compiler generates a static or dynamic binary from Cyber source code. This is done by first compiling Cyber code into C code, and using a C compiler to generate the final binary. The user can specify the system's cc compiler or the builtin tinyc compiler that is bundled with the CLI. This is currently in progress.