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. However, dynamic typing is also supported. If you're coming from a language such as Python, JavaScript, or Lua, it can be easier to get started with Dynamic Typing.
^topicHello World. #
use math
var worlds = {'World', '世界', 'दुनिया', 'mundo'}
worlds.append(math.random())
for worlds -> w:
print "Hello, $(w)!"
^topic
Syntax. #
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 braces) 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.
^topicLocal 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 will likely be removed in favor of context variables, constants, and extern
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.
A type specifier can be provided after the variable name, otherwise, it's inferred from the initializer:
var .my_map any = 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.
Context variables. #
Context variables are bound to each virtual thread and are accessible anywhere on the call stack. All fibers created in the same virtual thread reference the same context variables.
Context variables used in the program must be declared in the main source file with a default value: Planned Feature
-- main.cy
context MyInt = 123
context MyString = 'abc'
print MyInt --> 123
print MyString --> abc
To reference a context variable in a different source file, redeclare it with the same type:
-- foo.cy
context MyInt int
func foo():
print MyInt --> 123
Since this is a redeclaration, an assignment statement is not allowed.
Some context variables such as mem
for memory allocations are already declared in every program, so it only needs a redeclaration:
context mem Memory
var a = mem.new(int)
a.* = 123
print a.* --> 123
mem.free(a)
^topic
extern
variables. #
^topicPlanned Feature
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. #
^topicKeywords. #
There are 29
general keywords. This list categorizes them:
- Control Flow:
if
else
switch
case
while
for
break
continue
pass
- Operators:
or
and
not
- Variables:
var
context
- Functions:
func
return
- Fibers:
coinit
coyield
coresume
- Async:
await
- Types:
type
as
- Type embedding:
use
- Error Handling:
try
catch
throw
- Modules:
use
mod
- Dynamic Typing:
dyn
Contextual keywords. #
These keywords only have meaning in a certain context.
- Methods:
self
Self
- Types:
object
struct
cstruct
enum
trait
- Catching Errors:
caught
- Function Return:
void
Literals. #
- Boolean literal:
true
false
- Symbol literal:
symbol
- Error literal:
error
- None:
none
Operators. #
Cyber supports the following operators. They are ordered from highest to lowest precedence.
Operator | Description |
---|---|
<< >> | Bitwise left shift, right shift. |
& | Bitwise and. |
| || | Bitwise or, exclusive or. |
^ | Power. |
/ % * | Division, modulus, multiplication. |
+ - | Addition, subtraction. |
as | Type casting. |
> >= < <= != == | Greater, greater or equal, less, less or equal, not equals, equals. |
and | Logical and. |
or | Logical or. |
.. -.. | Range, reversed range. |
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.
^topicZero 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:
Type | Zero value |
---|---|
boolean | false |
int | 0 |
float | 0.0 |
String | '' |
List[T] | List[T]{} |
Map | Map{} |
type S | S{} |
@host type S | S.$zero() |
dyn | int(0) |
any | int(0) |
?S | Option[S].none |
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.
^topicCYON. #
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. #
In Cyber, there are value types and reference types. This section will describe common builtin types.
Booleans. #
Booleans can be true
or false
. See type bool
.
var a = true
if true:
print 'a is true'
^topic
Numbers. #
^topicIntegers. #
int
is the default integer type and is an alias for int64
.
It has 64-bits representing integers in the range -(263) to 263-1.
Integer types use the two's complement format.
The following integer types are supported: byte
, int8
, int16
, int32
, int64
.
Integer types are named according to how many bits they occupy.
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 an integer 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.
^topicFloats. #
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. #
^topicPlanned Feature
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.
^topicRaw 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:
Sequence | Code | Character |
---|---|---|
\0 | 0x00 | Null |
\a | 0x07 | Terminal bell |
\b | 0x08 | Backspace |
\e | 0x1b | Escape |
\n | 0x0a | Line feed |
\r | 0x0d | Carriage return |
\t | 0x09 | Horizontal tab |
\" | 0x22 | Double quote |
\\ | 0x5c | Backslash |
\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.
^topicString formatting. #
Values that can be formatted into a string will have a fmt
method:
var file = os.openFile('data.bin', .read)
var bytes = file.readAll()
-- 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:
- Ensures the use of a consistent line separator:
\n
- Allows lines to have a mix of raw string or string literals.
- Single quotes and double quotes do not need to be escaped.
- Allows each line to be indented along with the surrounding syntax.
- The starting whitespace for each line is made explicit.
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
^topicOptionals. #
An Optional is a value type that provides null safety by forcing the inner value to be unwrapped before it can be used.
The Option
template type is a choice type that either holds a none
value or contains some
value. The option template is defined as:
type Option[T type] 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
Arrays. #
An array type is a value type and is denoted as [N]T
where N
is the size of the array and T
is the element type.
See type Array
.
Arrays are initialized with its type followed by the initializer literal:
var a = [3]int{1, 2, 3}
The number of elements can be inferred using pseudo type [.]T
: Planned Feature
var a = [.]int{1, 2, 3}
The array type can be inferred by the dot initializer literal:
var a [3]int = .{1, 2, 3}
Arrays can be indexed:
a[2] = 300
print a[2] --> 300
^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
.
The first element of the list starts at index 0. Lists are initialized with elements in braces:
var list = {1, 2, 3}
print list[0] --> 1
The empty list is initialized with an underscore as the only element:
var list = {_}
print list.len() --> 0
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}
print list[0..0] --> {_}
print list[0..3] --> {1, 2, 3}
print list[3..] --> {4, 5}
print 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}
print 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)
Since List
is a generic type, an explicit List type can be attached to the initializer:
var a = List[int]{1, 2, 3}
When the intializer is only prefixed with a dot, it will infer the List type constraint:
var a List[int] = .{1, 2, 3}
^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.
^topicCheck field existence. #
^topicPlanned Feature
Prototypes. #
^topicPlanned Feature
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 symbol.
, followed by an identifier.
Each symbol has a global unique ID.
var currency = symbol.usd
print currency == .usd --> true
print int(currency) -->
^topic
any
. #
Unlike dyn
, 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
dyn
. #
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.
^topicType Declarations. #
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 dot 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 --> Option.none
type Student:
name String
age int
gpa float
var s = Student{}
print s.name -->
print s.age --> 0
print s.gpa --> 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 are functions that are invoked with a parent instance using the .
operator.
When the first parameter of a function contains self
, it's declared as a method of the parent type:
type Node:
value int
next ?Node
func inc(self, n):
self.value += n
func incAndPrint(self):
self.inc(321)
print value
var n = Node{value=123, next=none}
n.incAndPrint() -- Prints "444"
self
is then used to reference members of the parent instance.
Methods can be declared outside of the type declaration as a flat declaration:
func Node.getNext(self):
return self.next
^topic
Type functions. #
Type functions can be declared within the type block without a self
param:
type Node:
value int
next ?Node
func new():
return Node{value=123, next=none}
var n = Node.new()
Type functions can also be declared outside of the type block using a flat declaration:
type Node:
value int
next ?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(self) 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 are value types and do not have a reference count. They can be safely referenced with the ref
modifier and their lifetime can be managed with single ownership semantics. Unsafe pointers can not reference structs by default, but there may be an unsafe builtin to allow it anyway.
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
Tuples. #
Tuples can be declared using parentheses to wrap member fields:
type Vec2 struct(x float, y float)
If the fields share the same type, they can be declared in a field group:
type Vec3 struct(x, y, z float)
Function declarations can still be declared under the type:
type Vec2 struct(x float, y float):
func scale(self, s float):
self.x *= s
self.y *= s
Tuples can be initialized with member values corresponding to the order they were declared:
var v = Vec2{3, 4}
The initializer can also be inferred from the target type:
var v Vec2 = .{3, 4}
Tuples can still be initialized with explicit field names:
var v = Vec2{x=3, y=4}
^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. #
The general way to initialize a choice is to invoke the initializer with the payload as the argument:
var rect = Rectangle{width=10, height=20}
var s = Shape.rectangle(rect)
s = Shape.line(20)
If the payload is a record-like type, the choice can be initialized with a record literal:
var s = Shape.rectangle{width=10, height=20}
A choice without a payload is initialized like an enum member:
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
Unwrap choice. #
A choice can be accessed by specifying the unwrap operator .!
before the tagged member name. This will either return the payload or panic at runtime:
var s = Shape{line=20}
print s.!line --> 20
^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 declaration is denoted as type A -> T
where A
is the alias type of T
:
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(self, 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. #
A trait type defines a common interface for implementing types. A trait type is declared with the trait
keyword:
type Shape trait:
func area(self) float
Types can implement a trait using the with
keyword:
type Circle:
with Shape
radius float
func area(self) float:
return 3.14 * self.radius^2
type Rectangle:
with Shape
width float
height float
func area(self) float:
return self.width * self.height
A type that intends to implement a trait but does not satisfy the trait's interface results in a compile error.
Implementing types become assignable to the trait type:
var s Shape = Circle{radius=2}
print s.area() --> 12.57
s = Rectangle{width=4, height=5}
print s.area() --> 20
^topic
Type templates. #
Type declarations can include template parameters to create a type template:
type MyContainer[T type]:
id int
value T
func get(self) T:
return self.value
^topic
Expand type template. #
When the type template is invoked with template argument(s), a special 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.
^topicType specialization. #
Value templates can be used to specialize type templates:
def List[T type] type:
if T == dyn:
return DynList
else:
return GenList[T]
var a = List[int]{1, 2, 3}
^topic
C Types. #
C types interop with C-ABI types and are used to represent external or manual memory. Using C types is considered unsafe, but runtime safety checks can be inserted to make it safer at the cost of runtime performance.
C primitives. #
This table shows the size and type compatibility of Cyber and C types:
Cyber | C | Size (bits) |
---|---|---|
void | void | 0, used as *void |
int8 | int8_t | 8 |
byte | uint8_t | 8 |
c_char | char | 8 |
int16 | int16_t | 16 |
c_short | short | 16* |
c_uint16 | uint16_t | 16 |
c_ushort | unsigned short | 16* |
int32 | int32_t | 32 |
c_int | int | 32* |
c_uint32 | uint32_t | 32 |
c_uint | unsigned int | 32* |
int, int64 | int64_t | 64 |
c_long | long | 64* |
c_uint64 | uint64_t | 64 |
c_ulong | unsigned long | 64* |
c_longlong | long long | 64* |
c_ulonglong | unsigned long long | 64* |
float32 | float | 32 |
float, float64 | double | 64 |
c_longdouble | long double | 80* |
*T, pointer[T] | T* | 64* |
* Varies depending on C compiler and architecture. Pointers may occupy an extra word size for runtime memory checks.
^topicC structs. #
C structs
are unsafe since they can be referenced by unsafe pointers. The underlying memory could be corrupted or invalidated before use.
In contrast, structs can be safely used since they can only be instantiated in safe memory.
C structs
can be declared with the cstruct
keyword:
type Data cstruct:
x float
y float
ptr *int
str [*]byte
A C struct
may contain:
- C types.
- Primitive types.
- Container types that contain a compatible element type. This includes
enums
,choices
, andoptionals
.
It may not contain structs
or objects
.
C struct methods. #
C structs
can be declared with methods:
type Vec2 cstruct:
x float
y float
func add(self, o Vec2):
return Vec2{
x = self.x+o.x,
y = self.y+o.y,
}
var v = Vec2{x=1, y=2}
print v.add(Vec2{x=2, y=1}) --> Vec2{x=3, y=3}
In this example add
passes the receiver by value.
In order to pass the receiver by reference, self
must be annotated with *
:
type Vec2 cstruct:
x float
y float
func add(*self, o Vec2):
self.x += o.x
self.y += o.y
var v = Vec2{x=1, y=2}
v.add(Vec2{x=2, y=1})
print v --> Vec2{x=3, y=3}
^topic
C struct reference. #
The *
operator is used to obtain the pointer to a C struct
value:
type Vec2 cstruct:
x float
y float
var v = Vec2{x=1, y=2}
var ref = *v
ref.x = 4
print v --> Vec2{x=4, y=2}
The reference is a pointer type that points to the C struct
:
func scale(a *Vec2, n float):
a.x *= n
a.y *= n
add(*v, 10)
print v --> Vec2{x=40, y=20}
^topic
Pointers. #
A pointer
is a reference to a memory location. Its type is denoted as *T
where T
is the type that the pointer references in memory:
func setName(p *Person, name [*]byte):
p.name = name
var p = Person{}
setName(*p, 'Spock')
Depending on the target architecture, the alignment of the pointer will either be at least 8 bytes on a 64-bit machine or 4 bytes on a 32-bit machine. Aligned pointers will be supported in a future version.
Pointers are unsafe since they can reference corrupted or invalidated memory. When runtime memory checks are enabled, pointers will occupy an extra word size in order to set traps and prevent unsafe uses of pointers. See Memory / Runtime memory checks.
A pointer can be created with an explicit address using pointer
.
var ptr = pointer(void, 0xDEADBEEF)
print ptr.value() --'3735928559'
^topic
Dereferencing pointers. #
Pointers are dereferenced using the accessor operator .*
:
var a = 123
var ptr = *a
print ptr.* --> 123
ptr.* = 10
print a --> 10
^topic
Pointer indexing. #
The index operator is used to read or write to a particular element:
var a = mem.alloc(int, 10)
a[5] = 123
print a[5] --> 123
Negative indexing will locate the element before the pointer's address.
^topicPointer arithmetic. #
^topicPlanned Feature
Pointer casting. #
^topicPlanned Feature
Pointer slicing. #
The slice operator will produce a new slice of type [*]T
from a *T
pointer type:
var ints = mem.alloc(int, 10)
var slice = ints[0..5]
print typeof(slice) --> []int
^topic
Pointer conversions. #
^topicPlanned Feature
Pointer slices. #
Slices are pointers with a length field. They are denoted as [*]T
where T is the element type.
A slice can be created by taking the pointer of an array: Planned feature
var arr = [3]int{1, 2, 3}
var s = *arr
Read and write an element with the index operator:
print s[0] --> 1
s[1] = 123
print s[1] --> 123
Slices provide bounds checking when runtime safety is enabled: Planned feature
print s[100] --> Panic. Out of bounds.
Slice operations can be applied: Planned feature
print s[0..2] --> {1, 2}
^topic
Union types. #
^topicPlanned Feature
Control Flow. #
Cyber provides the common constructs to branch and enter loops.
Branching. #
^topicif
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. #
^topicInfinite 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}
.
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.
Deferred Execution. #
^topicPlanned Feature
Functions. #
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 dyn, 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) [2]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.
^topicClosures. #
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
extern
functions. #
^topicPlanned Feature
Function types. #
A function type is denoted as func(P1, P2, ...) R
where P
s are parameter types and R
is the return type in addition to any function modifiers:
type AddFn -> func(int, int) int
Function types can include optional parameter names:
type AddFn -> func(a int, b int) int
If one parameter has a name, the other parameters must also have names. Parameter names do not alter the function signature and only serve to document the function type.
Only static functions can be assigned to a function type:
func add(a int, b int) int:
return a + b
var fn AddFn = add
fn(10, 20) --> 30
Similarily, a function union type is denoted as Func(P1, P2, ...) R
and can store static functions, lambdas, and closures:
var c = 5
func addClosure(a int, b int) int:
return a + b + c
var fn Func(int, int) int = add
fn(10, 20) --> 30
fn = addClosure
fn(10, 20) --> 35
Function types declared in template parameters represent function symbol types. A function symbol can only be used at compile-time. It can be expanded to a runtime function:
type IntMap[K type, HASH func(K) int]
ints [*]int
-- Probing omitted.
func get(self, key K) int:
var slot = HASH(key) % self.ints.len
return self.ints[slot]
func my_str_hash(s String) int:
return s.len()
var m = IntMap[String, my_str_hash]{}
^topic
Parameter groups. #
When multiple parameters share the same type they can be declared together in a sequence:
func sum(a, b, c int) int
return a + b + c
^topic
Named parameters. #
^topicPlanned Feature
Variadic parameters. #
^topicPlanned Feature
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 block appends a lambda to a call expression's last argument:
func Button(name String, size int, on_click func() void) ButtonConfig:
return .{
name = name,
size = size,
on_click = on_click,
}
Button('Count', 10, ():
print 'on click'
^topic
Piping. #
^topicPlanned Feature
Compile-time call. #
Any function can be invoked at compile-time using #
before the arguments:
func fib(n int) int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
var res = fib#(30)
print res --> 832040
The result of the function call is used instead of emitting a runtime function call. The call arguments must be compile-time compatible values.
There is no need for #
if the caller is already in a compile-time context: Compile-time if is planned.
#if fib(30) == 832040:
print 'Correct result.'
^topic
Function templates. #
Function declarations can include template parameters to create a function template:
func add[T](T type, a T, b T) T:
return a + b
Only the template parameter names are declared. Their types are then inferred from the function signature.
^topicExplicit template call. #
When the function is invoked with template argument(s), a new runtime function is generated and used:
print add(int, 1, 2) --> 3
print add(float, 1, 2) --> 3.0
Note that invoking the function again with the same template argument(s) uses the same generated function. In other words, the generated function is always memoized from the template arguments.
^topicExpand function. #
The function template can be explicitly expanded to a runtime function:
var addInt = add[int]
print addInt(1, 2) --> 3
^topic
Infer param type. #
When a template parameter is first encountered in a function parameter's type specifier, it's inferred from the argument's type:
func add[T](a T, b T) T:
return a + b
print add(1, 2) --> 3
print add(1.0, 2.0) --> 3.0
In the above example, add[int]
and add[float]
were inferred from the function calls:
Nested template parameters can also be inferred:
func set[K, V](m Map[K, V], key K, val V):
m.set(key, val)
^topic
Modules. #
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.
^topicImport 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.
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. An imported module containing top-level statements returns an error:
-- main.cy
use a 'foo.cy'
print a.foo
-- foo.cy
use 'bar.cy'
var .foo = 123
print foo -- Error: Top-level statement not allowed.
^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.
^topicDestructure 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 *
.
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:
^topicmod 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()
func allTypes() List[type]
func bitcast(D type, val S) D
func copy(val T) T
Copies a primitive value or creates a shallow copy of an object value.
func choicetag(choice T) T.Tag
Returns the tag of a choice. TODO: This will be moved to
T.tag()
.
func dump(val any) void
Dumps a detailed description of a value.
func eprint(str any) void
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 genericOption(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) dyn
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 ptrcast(D type, val S) *D
func print(str any) void
Prints a value. The host determines how it is printed.
func queueTask(fn
) void Queues a callback function as an async task.
func runestr(val int) String
Converts a rune to a string.
func sizeof(T type) int
func sizeof_(type_id int) int
func typeOf(t ExprType) type
Returns the type of an expression.
func typeInfo(T type) TypeInfo
Returns info about a type.
^topic
func typeid(T type) int
type void
#
^topic
type bool
#
^topic
func bool.$call(val any) bool
Converts any value to either
true
orfalse
. Integers and floats equal to 0 return false. Empty strings return false. Otherwise, returns true.
type symbol
#
^topic
type error
#
func sym(self) symbol
Return the underlying
symbol
.
^topic
func error.$call(val any) error
Create an error from symbol.
type byte
#
func $prefix~(self) byte
func $infix<(self, o byte) bool
func $infix<=(self, o byte) bool
func $infix>(self, o byte) bool
func $infix>=(self, o byte) bool
func $infix+(self, o byte) byte
func $infix-(self, o byte) byte
func $infix*(self, o byte) byte
func $infix/(self, o byte) byte
func $infix%(self, o byte) byte
func $infix^(self, o byte) byte
func $infix&(self, o byte) byte
func $infix|(self, o byte) byte
func $infix||(self, o byte) byte
func $infix<<(self, o int) byte
func $infix>>(self, o int) byte
func fmt(self, format NumberFormat) String
Formats the byte using a NumberFormat.
func fmt(self, format NumberFormat, config Table) String
opts.pad
provides the ASCII rune that is used for padding with a string length ofconfig.width
.
^topic
func $call(b byte) byte
type int
#
func $prefix~(self) int
func $prefix-(self) int
func $infix<(self, o int) bool
func $infix<=(self, o int) bool
func $infix>(self, o int) bool
func $infix>=(self, o int) bool
func $infix+(self, o int) int
func $infix-(self, o int) int
func $infix*(self, o int) int
func $infix/(self, o int) int
func $infix%(self, o int) int
func $infix^(self, o int) int
func $infix&(self, o int) int
func $infix|(self, o int) int
func $infix||(self, o int) int
func $infix<<(self, o int) int
func $infix>>(self, o int) int
func fmt(self, format NumberFormat) String
Formats the integer using a NumberFormat.
func fmt(self, format NumberFormat, config Table) String
opts.pad
provides the ASCII rune that is used for padding with a string length ofconfig.width
.
^topic
func int.$call(val any) int
type float
#
func $prefix-(self) float
func $infix<(self, o float) bool
func $infix<=(self, o float) bool
func $infix>(self, o float) bool
func $infix>=(self, o float) bool
func $infix+(self, o float) float
func $infix-(self, o float) float
func $infix*(self, o float) float
func $infix/(self, o float) float
func $infix%(self, o float) float
func $infix^(self, o float) float
^topic
func float.$call(val any) float
Converts the value to a
float
. Panics if type conversion fails.
type placeholder2
#
^topic
type placeholder3
#
^topic
type taglit
#
^topic
type dyn
#
^topic
type any
#
^topic
type type
#
^topic
func id(self) int
Returns a unique ID for this type.
type IntInfo
#
^topic
type FloatInfo
#
^topic
type HostObjectInfo
#
^topic
type ObjectInfo
#
^topic
type ObjectField
#
^topic
type EnumInfo
#
^topic
type EnumCase
#
^topic
type ChoiceInfo
#
^topic
type ChoiceCase
#
^topic
type FuncInfo
#
^topic
type FuncParam
#
^topic
type ArrayInfo
#
^topic
type StructInfo
#
^topic
type StructField
#
^topic
type TraitInfo
#
^topic
type PointerInfo
#
^topic
type OptionInfo
#
^topic
func type.$call(val any) type
Return the type of a value. See
typeof
to obtain the type of an expression at compile-time.
type List
#
func $index(self, idx int) T
func $index(self, range Range) List[T]
func $setIndex(self, idx int, val T) void
func append(self, val T) void
Appends a value to the end of the list.
func appendAll(self, list List[T]) void
Appends the elements of another list to the end of this list.
func insert(self, idx int, val T) void
Inserts a value at index
idx
.
func iterator(self) ListIterator[T]
Returns a new iterator over the list elements.
func join(self, sep String) String
Returns a new string that joins the elements with
separator
.
func len(self) int
Returns the number of elements in the list.
func remove(self, idx int) void
Removes an element at index
idx
.
func resize(self, size int) void
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 resize_(self, elem_t int, size int) void
func sort(self, lessFn
) void Sorts the list with the given
less
function. If elementa
should be ordered beforeb
, the function should returntrue
otherwisefalse
.
^topic
func List.fill(val T, n int) List[T]
Creates a list with initial capacity of
n
and values set toval
. If the value is an object, it is shallow copiedn
times.
type ListIterator
#
func next(self) ?T
^topic
func next_(self, ret_t int) ?T
type Tuple
#
^topic
func $index(self, idx int) any
type FuncSig
#
^topic
type funcptr_t
#
^topic
type funcunion_t
#
^topic
type Func
#
^topic
type funcsym_t
#
^topic
type Table
#
func $initPair(self, key String, value any) void
func $get(self, name String) dyn
func $set(self, name String, value any)
func $index(self, key any) dyn
^topic
func $setIndex(self, key any, value any) void
type Map
#
func $initPair(self, key any, value any) void
func $index(self, key any) dyn
func $setIndex(self, key any, val any) void
func contains(self, key any) bool
Returns whether there is a value mapped to
key
.
func get(self, key any) ?any
Returns value mapped to
key
or returnsnone
.
func remove(self, key any) bool
Removes the element with the given key
key
.
func size(self) int
Returns the number of key-value pairs in the map.
^topic
func iterator(self) MapIterator
Returns a new iterator over the map elements.
type MapIterator
#
^topic
func next(self) ?any
type String
#
func $index(self, idx int) int
Returns the rune at byte index
idx
. The replacement character (0xFFFD) is returned for an invalid UTF-8 rune.
func $index(self, range Range) String
Returns a slice into this string from a
Range
withstart
(inclusive) toend
(exclusive) byte indexes.
func $infix+(self, o any) String
Returns a new string that concats this string and
str
.
func concat(self, o String) String
Returns a new string that concats this string and
str
.
func count(self) int
Returns the number of runes in the string.
func endsWith(self, suffix String) bool
Returns whether the string ends with
suffix
.
func find(self, needle String) ?int
Returns the first byte index of substring
needle
in the string ornone
if not found. SIMD enabled.
func findAnyByte(self, set List[byte]) ?int
Returns the first index of any byte in
set
ornone
if not found.
func findAnyRune(self, runes List[int]) ?int
Returns the first byte index of any rune in
runes
ornone
if not found. SIMD enabled.
func findByte(self, b byte) ?int
Returns the first index of
byte
in the array ornone
if not found.
func findRune(self, rune int) ?int
Returns the first byte index of a rune
needle
in the string ornone
if not found. SIMD enabled.
func fmtBytes(self, format NumberFormat) String
Formats each byte in the string using a NumberFormat. Each byte is zero padded.
func getByte(self, idx int) byte
Returns the byte value (0-255) at the given index
idx
.
func getInt(self, 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(self, 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(self, idx int, str String) String
Returns a new string with
str
inserted at byte indexidx
.
func insertByte(self, idx int, byte int) String
Returns a new array with
byte
inserted at indexidx
.
func isAscii(self) bool
Returns whether the string contains all ASCII runes.
func len(self) int
Returns the byte length of the string. See
count()
to obtain the number of runes.
func less(self, other String) bool
Returns whether this string is lexicographically before
other
.
func lower(self) String
Returns this string in lowercase.
func replace(self, needle String, replacement String) String
Returns a new string with all occurrences of
needle
replaced withreplacement
.
func repeat(self, n int) String
Returns a new string with this string repeated
n
times.
func seek(self, idx int) int
Returns the starting byte index for the rune index
idx
.
func sliceAt(self, idx int) String
Returns the UTF-8 rune starting at byte index
idx
as a string.
func split(self, sep String) List[String]
Returns a list of UTF-8 strings split at occurrences of
sep
.
func startsWith(self, prefix String) bool
Returns whether the string starts with
prefix
.
func trim(self, mode symbol, delims String) String
Returns the string with ends trimmed from runes in
delims
.mode
can be .left, .right, or .ends.
func upper(self) String
Returns this string in uppercase.
^topic
func String.$call(val any) String
Converts a value to a string.
type array_t
#
^topic
type Array
#
func iterator(self) RefSliceIterator[T]
Returns a new iterator over the array.
^topic
func len(self) int
Returns the number of elements in the array.
type pointer
#
func $index(self, idx int) *T
func $index(self, range Range) [*]T
func $setIndex(self, idx int, val T) void
func addr(self) int
When pointer runtime safety is enabled, this returns the raw pointer address as an
int64
. Otherwise, the pointer itself is bitcasted to anint64
.
func asObject(self) any
Casts the pointer to a Cyber object. The object is retained before it's returned.
func fromCstr(self, offset int) String
Returns a
String
from a null terminated C string.
func get(self, offset int, ctype symbol) dyn
Dereferences the pointer at a byte offset and returns the C value converted to Cyber.
func getString(self, offset int, len int) String
Returns a
String
with a copy of the byte data starting from an offset to the specified length.
func set(self, offset int, ctype symbol, val any) void
Converts the value to a compatible C value and writes it to a byte offset from this pointer.
^topic
func pointer.$call(T type, addr int) *T
Converts an
int
to apointer
value.
type ExternFunc
#
func addr(self) 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
func ptr(self) *void
type Fiber
#
^topic
func status(self) symbol
type Range
#
^topic
type TccState
#
^topic
type Future
#
func Future.complete(val T) Future[T]
Returns a
Future[T]
that has a completed value.
^topic
func Future.new(T type) Future[T]
type FutureResolver
#
func complete(self, val T) void
func future(self) Future[T]
^topic
func FutureResolver.new(T type) FutureResolver[T]
type Ref
#
^topic
type RefSlice
#
func $index(self, range Range)
func $setIndex(self, idx int, val T) void
func iterator(self) RefSliceIterator[T]
^topic
func len(self) int
type RefSliceIterator
#
^topic
func next(self) ?T
type PtrSlice
#
func $index(self, idx int) *T
func $index(self, range Range) [*]T
func $setIndex(self, idx int, val T) void
func endsWith(self, suffix [*]T) bool
Returns whether the array ends with
suffix
.
func findScalar(self, needle T) ?int
Returns the first index of
needle
in the slice ornone
if not found.
func iterator(self) PtrSliceIterator[T]
func len(self) int
^topic
func startsWith(self, prefix [*]T) bool
Returns whether the array starts with
prefix
.
type PtrSliceIterator
#
^topic
func next(self) ?T
type IMemory trait
#
func alloc(self, len int) [*]byte
^topic
func free(self, buf [*]byte) void
type Memory
#
func Memory.new(self, T type) *T
func Memory.alloc(self, T type, n int) [*]T
func Memory.free(self, ptr *T)
^topic
func Memory.free_(self, slice [*]T)
type DefaultMemory
#
func alloc(self, len int) [*]byte
^topic
func free(self, buf [*]byte) void
type ExprType
#
^topic
func getType(self) type
mod cy
#
The cy
module contains functions related to the Cyber language.
Sample usage:
use cy
print cy.toCyon([1, 2, 3])
var Success int
var Await int
var ErrorCompile int
var ErrorPanic int
var TypeVoid int
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) void
Starts an isolated REPL session. The callback
read_line(prefix String) String
is responsible for obtaining the input.
^topic
func toCyon(val any) String
Encodes a value to CYON string.
type EvalConfig
#
^topic
type EvalResult
#
^topic
type Value
#
func dump(self) String
func getTypeId(self) int
^topic
func toHost(self) any
type VM
#
func eval(self, code String) EvalResult
func eval(self, uri String, code String, config EvalConfig) EvalResult
func getErrorSummary(self) String
func getPanicSummary(self) String
^topic
func VM.new() VM
Create an isolated VM.
type REPL
#
func printIntro(self)
func read(self, read_line dyn) ?String
func evalPrint(self, code String) void
func getPrefix(self) String
^topic
func REPL.new() REPL
mod math
#
The math module contains commonly used math constants and functions.
Sample usage:
use math
var r = 10.0
print(math.pi * r^2)
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.
^topic
func trunc(a float) float
Returns the integer portion of x, removing any fractional digits.
Std modules. #
Std modules come with Cyber's CLI. They include:
^topicmod cli
#
The cli
module contains functions related to the command line.
Sample usage:
use cli
cli.repl()
func repl() void
Starts an isolated REPL session. Invokes
cy.repl(replReadLine)
.
^topic
func replReadLine(prefix String) String
Default implementation to read a line from the CLI for a REPL.
mod 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)"
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) void
Attempts to access a file at the given
path
with the.read
,.write
, or.readWrite
mode. Throws an error if unsuccessful.
func args() List[String]
Returns the command line arguments in a
List[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 fromurl
.
func copyFile(srcPath String, dstPath String) void
Copies a file to a destination path.
func createDir(path String) void
Creates the directory at
path
. Returnstrue
if successful.
func createFile(path String, truncate bool) File
Creates and opens the file at
path
. Iftruncate
is true, an existing file will be truncated.
func cstr(s String) *void
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[String]) Map
Runs a shell command and returns the stdout/stderr.
func exePath() String
Returns the current executable's path.
func exit(status int) void
Exits the program with a status code.
func fetchUrl(url String) String
Fetches the contents at
url
using the HTTP GET request method.
func free(ptr *void) void
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) *void
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[dyn]) Table
Given expected
ArgOption
s, returns aTable
of the options and arest
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, useFile.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, usestdin
.
func realPath(path String) String
Returns the absolute path of the given path.
func removeDir(path String) void
Removes an empty directory at
path
. Returnstrue
if successful.
func removeFile(path String) void
Removes the file at
path
. Returnstrue
if successful.
func setEnv(key String, val String) void
Sets an environment variable by key.
func sleep(ms float) void
Pauses the current thread for given milliseconds.
func unsetEnv(key String) void
Removes an environment variable by key.
^topic
func writeFile(path String, contents String) void
Writes
contents
as a string or bytes to a file.
type File
#
func close(self) void
Closes the file handle. File ops invoked afterwards will return
error.Closed
.
func iterator(self) File
func next(self) String
func read(self, n int) String
Reads at most
n
bytes as anArray
.n
must be at least 1. A result with length 0 indicates the end of file was reached.
func readAll(self) String
Reads to the end of the file and returns the content as an
Array
.
func seek(self, n int) void
Seeks the read/write position to
pos
bytes from the start. Negativepos
is invalid.
func seekFromCur(self, n int) void
Seeks the read/write position by
pos
bytes from the current position.
func seekFromEnd(self, n int) void
Seeks the read/write position by
pos
bytes from the end. Positivepos
is invalid.
func stat(self) Map
Returns info about the file as a
Map
.
func streamLines(self) File
Equivalent to
streamLines(4096)
.
func streamLines(self, bufSize int) File
Returns an iterable that streams lines ending in
\n
,\r
,\r\n
, or theEOF
. The lines returned include the new line character(s). A buffer size ofbufSize
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
.
^topic
func write(self, val String) int
Writes a
String
at the current file position. The number of bytes written is returned.
type Dir
#
func iterator(self) 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(self) Map
Returns info about the file as a
Map
.
^topic
func walk(self) 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.
type DirIterator
#
^topic
func next(self) ?Map
type FFI
#
func bindCallback(self, fn any, params List[dyn], 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(self, path ?String) any
Calls
bindLib(path, [:])
.
func bindLib(self, 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. Ifconfig
containsgen_table: true
, aTable
is returned instead with C-functions binded as function values.
func bindObjPtr(self, obj any) *void
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(self, mt type, fields List[dyn]) void
Binds a Cyber type to a C struct.
func cfunc(self, name String, params List[dyn], ret any) void
Declares a C function which will get binded to the library handle created from
bindLib
.
func new(self, ctype symbol) *void
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 acpointer
instead which will be more idiomatic to use.
^topic
func unbindObjPtr(self, obj any) void
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.
type CArray
#
^topic
type CDimArray
#
^topic
Map DirEntry
#
key | summary |
---|---|
'name' -> Array | The name of the file or directory. |
'type' -> #file | #dir | #unknown | The type of the entry. |
Map DirWalkEntry
#
key | summary |
---|---|
'name' -> Array | The name of the file or directory. |
'path' -> Array | The path of the file or directory relative to the walker's root directory. |
'type' -> #file | #dir | #unknown | The type of the entry. |
Table ArgOption
#
key | summary |
---|---|
'name' -> String | The name of the option to match excluding the hyphen prefix. eg. -path |
'type' -> metatype(String | float | boolean) | Parse as given value type. |
'default' -> any | Optional: Default value if option is missing. none is used if this is not provided. |
mod test
#
The test
module contains utilities for testing.
Sample usage:
use t 'test'
var a = 123 + 321
t.eq(a, 444)
func assert(pred bool) void
Panics if
pred
isfalse
.
func eq(a T, b T) bool
Returns whether two values are equal. Panics with
error.AssertError
if types or values do not match up.
func eqList(a List[T], b List[T]) bool
Returns
true
if two lists have the same size and the elements are equal as ifeq
was called on those corresponding elements.
func eqSlice(a
, b ) bool Returns
true
if two slices have the same size and the elements are equal as ifeq
was called on those corresponding elements.
func eqNear(a T, b T) bool
Returns
true
if two numbers are near each other within epsilon 1e-5.
func fail()
func fail(msg String)
^topic
func throws(fn any, err error)
Asserts that an error was thrown when invoking a function.
FFI. #
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
:
dyn lib = ffi.bindLib('./mylib.so')
lib.add(123, 321)
Note that dyn
is used to allow lib
to be used dynamically since the type is unknown at compile-time.
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.
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.
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.
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.
Binding | Cyber | C |
---|---|---|
.bool | bool | bool |
.char | int | int8_t, signed char |
.uchar | int | uint8_t, unsigned char |
.short | int | int16_t, short |
.ushort | int | uint16_t, unsigned short |
.int | int | int32_t, int |
.uint | int | uint32_t, unsigned int |
.long | int | int64_t, long long |
.ulong | int | uint64_t, unsigned long long |
.usize | int | size_t, uintptr_t |
.float | float | float |
.double | float | double |
(1) .charPtr | *void | char* |
.voidPtr | *void | void* |
(2) type {S} | type {S} | struct |
- Use
os.cstr()
andpointer.fromCstr()
to convert between a Cyber string and a null terminated C string. - The mapping from a Cyber object type
S
and the C-struct can be declared withcbind
.
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 *void
c bool
ffi.cbind(MyObject, {.float, .voidPtr, .bool})
ffi.cfunc('foo', {MyObject}, MyObject)
dyn 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)
dyn lib = ffi.bindLib('./mylib.so')
var ptr = lib.foo(MyObject{a=123, b=os.cstr('foo'), c=true})
var res = lib.ptrToMyObject(ptr)
^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.
^topicError Handling. #
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. #
^topicerror
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
^topicerror
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. #
^topictry
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. #
^topicThrows modifier. #
The throws modifier !
indicates that a function contains a throwing expression that was not caught with try catch
.
The modifier 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 modifier 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 modifier 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.
^topicPanics. #
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.
Concurrency. #
Cyber supports asynchronous and cooperative concurrency.
Async. #
Cyber supports asynchronous execution which provides preemptive concurrency. Tasks can be scheduled over a single thread. Multi-threaded execution is planned.
^topicFutures. #
A Future
is a promise that some work will either complete or fail at some point in the future.
This abstraction allows the current thread to continue execution without waiting for the completion of the future.
The asynchronous work encapsulated by the future has the opportunity to run in parallel. For example, I/O bound work can be delegated to the operating system and CPU bound work can be run across multiple threads. Some work may simply run on the same thread.
If an API function is meant to do work asynchronously, it would return a Future
:
use aio
var f = aio.delay(1000)
print f --> Future(void)
Futures can hold a result value when they are completed:
use aio
var f = aio.readFile('foo.txt')
print f --> Future(String)
Futures can be created with a completed value:
var f = Future.complete(100)
print f --> Future(int)
print f.get().? --> 100
Future are also created when composing them together.
^topicawait
. #
The await
expression asynchronously waits for a Future
to resolve.
It guarantees that the current execution flow will not resume until the future is completed.
Once resumed, the expression evaluates to the completed result:
use aio
var res = await aio.readFile('foo.txt')
print res --> bar
In the above example, the file contents of foo.txt
is "bar".
await
suspends the current fiber so the scheduler can execute tasks in its ready queue.
When the future resolves, the suspended task is moved to the ready queue waiting to resume execution.
Performing await
on other values besides a Future
type will evaluate to the value and will not perform any waiting:
var v = await 123
print v --> 123
^topic
Colorless async. #
await
can be used in any function.
This means that async functions are colorless and don't require a special function modifier.
Future chains. #
Future.then
is used to attach a callback that will only be invoked when the future completes, thereby creating an asynchronous chain: Planned Feature
use aio
var f = aio.readFile('foo.txt').then() -> res:
print res --> bar
print f --> Future(void)
By default Future.then
evaluates to a new Future(void)
and represents the completion of the callback.
A callback that returns a result requires a generic parameter or inferred from the lambda: Planned Feature
var f = aio.readFile('foo.txt').then() -> res:
return res.len()
print f --> Future(int)
print await f --> 3
Similarily, Future.catch
attaches a callback that will only be invoked when the future fails: Planned Feature
var f = aio.readFile('foo.txt').catch() -> err:
print err --> error.FileNotFound
^topic
Resolving futures. #
FutureResolver
produces a completable Future
. In the following example, a future is completed with the FutureResolver
after a queued task runs:
var r = FutureResolver[int].new()
queueTask():
r.complete(234)
var v = await r.future()
print v
^topic
Structured concurrency. #
Planned Feature
Asychronous tasks must be created with an Async
context which groups together related tasks.
This gives tasks well-defined lifetimes and allows policies to be applied to a task group.
Every program begins by pushing a new Async.waitAll
context to the async
context variable.
waitAll
is a finish policy that waits for all tasks to complete before it marks its finish task as completed.
In the following example, the created tasks are attached to the current Async
context:
use aio
aio.delay(1000)
addMoreTasks()
Since the program did not explicitly wait for the completion of the current Async
context, execution will end before 1 second has elapsed.
To wait for all the tasks to complete, the program can wait for the Async
context to finish:
use aio
context async Async
aio.delay(1000)
addMoreTasks()
await async.finish()
The context variable async
is declared to access the current Async
context.
finish
applies the finish policy waitAll
which completes when all tasks under the async context have been completed.
After invoking finish
, new tasks can no longer be created under the async context.
Doing so would result in a runtime error.
A future is returned to indicate the completion of the finish task.
Explicitly invoking finish
isn't idiomatic and was only demonstrated to show how an async context works. The same feat can be achieved with helpers that can wrap a block of related async logic:
use aio
await Async.block(.waitAll):
aio.delay(1000)
addMoreTasks()
Async.block
begins by creating a new async context and pushing the context to async
.
The callback is then invoked which creates a new task under the async
context.
Once the callback returns, the async
context and the result of Async.finish
is returned.
Finish policies. #
^topicPlanned Feature
Task cancellation. #
^topicPlanned Feature
Threads. #
^topicPlanned Feature
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.
^topicCreating 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 task = coinit(foo)
print count -- '0'
coresume task
print count -- '1'
coresume task
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
.
Passing arguments. #
Arguments after the callee are passed into the entry function:
var count = 0
var increment = func (inc):
count += inc
var task = coinit(increment, 5)
coresume task
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.
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.
task.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 task.status() != .done:
res = coresume fiber
print res
task.reset()
task.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 task = coinit:
count += 1 -- `count is captured`
coyield
count += 1
print count -- '0'
coresume task
print count -- '1'
coresume task
print count -- '2'
Referencing parent variables from the fiber block automatically captures them just like a function closure.
^topicPause 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 task = coinit(foo)
coresume task
coresume
also returns the resulting value.
func foo():
return 123
var task = coinit(foo)
print(coresume task) -- '123'
coyield
can return a value back to coresume
. Planned Feature
Fiber state. #
Use Fiber.status()
to get the current state of the fiber.
func foo():
coyield
print 'done'
var task = coinit(foo)
print task.status() -- '.paused'
coresume task
print task.status() -- '.paused'
coresume task
print task.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.
^topicGas mileage. #
^topicPlanned Feature
Dynamic Typing. #
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 dyn
keyword is used exclusively for dynamic declarations.
Dynamic variables. #
Variables declared with dyn
are implicitly given the dyn
type:
dyn 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:
dyn 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:
dyn 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:
dyn 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 tables. #
The builtin Table
type is used to create dynamic objects.
Tables are initialized with the record literal:
dyn a = {}
a.name = 'Nova'
print a.name --> Nova
Read more about how to use Tables.
^topicDynamic inference. #
When the inference tag is used in a dynamic context, it will attempt to resolve its value at runtime.
In this example, the dynamic value a
resolves to a String
at runtime and invokes the typed method trim
.
.left
then infers to the correct value at runtime:
print str.trim(.left, ' ')
^topic
Metaprogramming. #
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+'(self, o Vec2) Vec2:
return Vec2{
x = x + o.x,
y = y + o.y,
}
func '$prefix-'(self) 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(self, idx):
return arr[idx * 2]
func $setIndex(self, 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:
Operator | Name |
---|---|
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 |
Custom operators. #
^topicPlanned Feature
Magic functions. #
^topicCall 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. #
^topicPlanned Feature
$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(self, 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.
$get
method. #
The $get
method allows overriding field accesses for undeclared fields:
type Foo:
func $get(self, 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(self, 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(self, 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
Attributes. #
Attributes start with @
. They are used as declaration modifiers.
^topic
@host
Bind a function, variable, or type to the host. See libcyber.
Templates. #
Templates enables parametric polymorphism for types and functions. Template arguments are passed to templates to generate specialized code. This facilitates developing container types and algorithms that operate on different types.
See Type Declarations / Type templates and Functions / Function templates.
^topicValue templates. #
A value template returns a memoized value after being invoked with template arguments at compile-time. It's declared with def
:
def StrType[ID String] type:
if ID == 'bool':
return bool
else ID == 'int':
return int
else ID == 'String':
return String
else
throw error.Unsupported
var a StrType['int'] = 123
print a --> 123
This can be useful to evaluate compile-time logic to create new types or specialize other templates. Any compile-time compatible type can also be returned.
^topicMacros. #
^topicPlanned Feature
Compile-time execution. #
^topicPlanned Feature
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
^topic
type float64_t
Builtin functions. #
^topic
func genLabel(name String)
Emits a label during codegen for debugging.
Builtin constants. #
^topic
var modUri String
Evaluates to the module's URI as a string. See Module URI.
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. #
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. #
^topicCreate 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.
Eval script. #
clEval
compiles and evaluates a script:
CLStr src = STR(
"var a = 1\n"
"print(a + 2)\n"
);
CLValue val;
CLResultCode res = clEval(vm, src, &val);
if (res == CL_SUCCESS) {
printf("Success!\n");
clRelease(vm, val);
} else {
CLStr report = clNewLastErrorReport(vm);
printf("%s\n", report);
clFree(vm, 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.
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, CLModule* res) {
if (strncmp("my_mod", spec.buf, spec.len) == 0) {
CLStr src = STR(
"@host func add(a float, b float) float\n"
"@host var .MyConstant float\n"
"@host var .MyList List[dyn]\n"
"\n"
"@host\n"
"type MyNode _:\n"
" @host func asList(self) any"
"\n"
"@host func MyNode.new(a any, b any) MyNode\n"
);
*res = clCreateModule(vm, spec, src);
CLModuleConfig config = (CLModuleConfig){
.funcs = (CLSlice){ .ptr = funcs, .len = 3 },
.types = (CLSlice){ .ptr = types, .len = 1 },
.varLoader = varLoader,
};
clSetModuleConfig(vm, *res, &config);
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.
^topicDefault 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
.
Bind functions. #
An array of function definitions can be assigned to CLModuleConfig.funcs
.
When a @host
function is encountered by the compiler, it will use this mapping to find the correct function pointer:
CLHostFuncEntry funcs[] = {
CL_FUNC("add", add),
CL_FUNC("MyNode.asList", myNodeAsList),
CL_FUNC("MyNode.new", myNodeNew),
};
A fallback function loader can be assigned to CLModuleConfig.func_loader
.
It's invoked if a function could not be found in CLModuleConfig.funcs
.
bool funcLoader(CLVM* vm, CLFuncInfo info, CLFuncResult* out) {
// Check that the name matches before setting the function pointer.
if (strncmp("missing_func", info.name.buf, info.name.len) == 0) {
out->ptr = myMissingFunc;
return true;
} else {
return false;
}
}
^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
.
Bind types. #
An array of type definitions can be assigned to CLModuleConfig.types
.
When a @host
type is encountered by the compiler, it will use this mapping to initialize the type:
CLTypeId myNodeId;
CLHostTypeEntry types[] = {
CL_CUSTOM_TYPE("MyNode", &myNodeId, myNodeGetChildren, myNodeFinalizer),
};
When binding to the "MyNode" type, it's type id is saved to myNodeId
. This id is then used to create new instances of this type. See Host types.
A fallback type loader can be assigned to CLModuleConfig.type_loader
.
It's invoked if a type could not be found in CLModuleConfig.types
.
bool typeLoader(CLVM* vm, CLTypeInfo info, CLTypeResult* out) {
if (strncmp("MissingType", info.name.buf, info.name.len) == 0) {
out->type = CS_TYPE_OBJECT;
out->data.object.outTypeId = &myNodeId;
out->data.object.getChildren = myNodeGetChildren;
out->data.object.finalizer = myNodeFinalizer;
return true;
} else {
return false;
}
}
^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.
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 MyNode {
CLValue val1;
CLValue val2;
int a;
double b;
} MyNode;
// Implement the `new` function in MyNode.
CLValue myNodeNew(CLVM* vm, const CLValue* args, uint8_t nargs) {
// Instantiate our object.
CLValue new = clNewHostObject(vm, myNodeId, sizeof(MyNode));
MyNode* my = (MyNode*)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.
getChildren
#
Since MyNode
contains CLValue
children, the Type loader requires a getChildren
callback so that memory management can reach them:
CLValueSlice myNodeGetChildren(CLVM* vm, void* obj) {
MyNode* my = (MyNode*)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 myNodeFinalizer(CLVM* vm, void* obj) {
printf("MyNode finalizer was called.\n");
}
^topic
Memory. #
Cyber provides memory safety by default with structured and automatic memory. Manual memory is also supported but discouraged.
Structured memory. #
Structured memory is very much incomplete. It will be centered around single value ownership but the semantics are subject to change. Cyber uses single value ownership and variable scopes to determine the lifetime of values and references. When lifetimes are known at compile-time, the memory occupied by values do not need to be manually managed which prevents memory bugs such as:
- Use after free.
- Use after invalidation.
- Free with wrong allocator.
- Double free.
- Memory leaks.
- Null pointer dereferencing.
At the same time, structured memory allows performant code to be written since it provides safe semantics to directly reference values and child values. These safety features are guaranteed for debug and optimized builds with no additional runtime cost.
^topicValue ownership. #
Every value in safe memory has a single owner. An owner can be a variable that binds to a value. Otherwise, the owner can be a parent value or the value itself. The owner is responsible for deinitializing or dropping the value when it has gone out of scope (no longer reachable). For example, at the end of the block, a variable can no longer be accessed so it drops the value that it owns:
var a = 123
print a --> 123
-- Deinit `a`.
In this case, there is nothing to deinitialize since the value is an integer.
If the value was a String
, the deinit logic would release (-1) on a reference counted byte buffer since strings are just immutable views over byte buffers:
var a = 'hello'
print a --> hello
-- Deinit `a`.
-- `a.buf` is released.
-- `a.buf` is freed.
Since the string buffer's reference count reaches 0, it's freed as well.
Finally, let's take a look at ListValue
which manages a dynamically sized array of elements:
var a = ListValue[int]{1, 2, 3}
print a --> {1, 2, 3}
-- Deinit `a`.
-- `a.buf` is freed.
When a
is deinitialized, the buffer that holds the 3 integer elements is freed.
You may have surmised that it's named ListValue
because it's a value type (it can only be passed around by copying itself). The object type, List
, wraps ListValue
and can be passed around by reference.
The concept of a value having a single owner is very simple yet powerful. A value can represent any data structure from primitives to dynamically allocated buffers. A value always knows how to deinitialize itself, and the owner knows when to deinitialize the value. Later, we'll see that this same concept also applies to shared ownership.
^topicCopy semantics. #
By default, values are passed around by copying (shallow copying), but not all values can perform a copy.
A primitive, such as an integer, can always be copied:
var a = 123
var b = a
-- Deinit `b`.
-- Deinit `a`.
After a copy, a new value is created and given the owner of b
. At the end of the block, both a
and b
are deinitialized (which does nothing since they are just primitives).
Strings are also copyable since they are immutable views over byte buffers:
var a = 'hello'
var b = a
-- Deinit `b`.
-- `b.buf` is released.
-- Deinit `a`.
-- `a.buf` is released.
-- `a.buf` is freed.
The copy b
also reuses the byte buffer of a
by retaining (+1) on the reference counted byte buffer. The byte buffer is finally freed once there are no references pointing to it.
Unlike the integer and string, a ListValue
can not be copied since doing so requires duping a heap allocated buffer which is considered expensive:
var a = ListValue[int]{1, 2, 3}
var b = a --> error: Can not copy `ListValue`. Can only be cloned or moved.
Instead a
can only be cloned or moved.
By default, a declared value type is copyable if all of it's members are also copyable:
type Foo struct:
a int
b String
var a = Foo{a=123, b='hello'}
var b = a
Since integers and strings are both copyable, Foo
is also copyable.
Foo
is non-copyable if it contains at least one non-copyable member:
type Foo struct:
a int
b String
c ListValue[int]
var a = Foo{a=123, b='hello'}
var b = a --> error: Can not copy `Foo`. Can only be moved.
Foo
is also non-copyable if it contains unsafe types such as pointers or pointer slices:
type Foo struct:
a int
b String
c *Bar
d [*]float
Foo
can implement Copyable
to override the default behavior and define it's own copy logic:
type Foo struct:
with Copyable
a int
b String
c *Bar
d [*]float
func copy(self) Foo:
return .{
a = self.a,
b = self.b,
c = self.c,
d = self.d,
}
Likewise, Foo
can implement NonCopyable
which indicates that it can never be copied:
type Foo struct:
with NonCopyable
a int
b String
^topic
Cloning. #
Some value types are not allowed to be copied by default and must be cloned instead:
var a = ListValue[int]{1, 2, 3}
var b = a.clone()
Any Copyable
type is also Cloneable
. For example, performing a clone on an integer will simply perform a copy:
var a = 123
var b = a.clone()
A value type can implement Cloneable
to override the default behavior and define it's own clone logic:
type Foo struct:
with Cloneable
a int
b String
func clone(self) Foo:
return .{
a = self.a + 1,
b = self.b,
}
Likewise, Foo
can implement NonCloneable
which indicates that it can never be cloned:
type Foo struct:
with NonCloneable
a int
b String
^topic
Moving. #
Values can be moved, thereby transfering ownership from one variable to another:
var a = 123
var b = move a
print a --> error: `a` does not own a value.
Some types such as ListValue
can not be passed around by default without moving (or cloning) the value:
var a = ListValue[int]{1, 2, 3}
print computeSum(move a) --> 6
In this case, the list value is moved into the computeSum
function, so the list is deinitialized in the function before the function returns.
References. #
References are safe pointers to values. Unlike unsafe pointers, a reference is never concerned with when to free or deinitialize a value since that responsibility always belongs to the value's owner. They are considered safe pointers because they are guaranteed to point to their values and never outlive the lifetime of their values.
References grant in-place mutability which allows a value to be modified as long as it does not invalidate other references. Multiple references can be alive at once as long as an exclusive reference is not also alive.
The &
operator is used to obtain a reference to a value:
var a = 123
var ref = &a
ref.* = 234
print a --> 234
A reference can not outlive the value it's referencing:
var a = 123
var ref = &a
if true:
var b = 234
ref = &b --> error: `ref` can not outlive `b`.
A reference type is denoted as &T
where T
is the type that the reference points to:
var a = 123
func inc(a &int):
a.* = a.* + 1
References allow in-place mutation:
var a = ListValue[int]{1, 2, 3}
var third = &a[2]
third.* = 300
print a --> {1, 2, 300}
The element that third
points to can be mutated because it does not invalidate other references.
References however can not perform an unstable mutation. An unstable mutation requires an exclusive reference:
var a = ListValue[int]{1, 2, 3}
var ref = &a
ref.append(4) --> error: Expected exclusive reference.
^topic
Exclusive reference. #
An exclusive reference grants full mutability which allows a value to be modified even if could potentially invalidate unsafe pointers.
A single exclusive reference can be alive as long as no other references are also alive. Since no other references (safe pointers) are allowed to be alive at the same time, no references can become invalidated.
The &!
operator is used to obtain an exclusive reference to a value.
An exclusive reference type is denoted as &!T
where T
is the type that the reference points to.
ListValue
is an example of a type that requires an exclusive reference for operations that can resize or reallocate its dynamic buffer:
var a = ListValue[int]{1, 2, 3}
a.append(4)
print a --> {1, 2, 3, 4}
Note that invoking the method append
here automatically obtains an exclusive reference for self
without an explicit &!
operator.
If another reference is alive before append
, the compiler will not allow an exclusive reference to be obtained from a
.
Doing so would allow append
to potentially reallocate its dynamic buffer, thereby invalidating other references:
var a = ListValue[int]{1, 2, 3}
var third = &a[2]
a.append(4) --> error: Can not obtain exclusive reference, `third` is still alive.
print third
^topic
self
reference. #
By default self
has a type of &T
when declared in a value T
's method:
type Pair struct:
a int
b int
func sum(self) int:
return self.a + self.b
If self
requires an exclusive reference, then it must be prepended with !
:
type Pair struct:
a int
b int
func sum(!self) int:
return self.a + self.b
Invoking methods automatically obtains the correct reference as specified by the method:
var p = Pair{a=1, b=2}
print p.sum() --> 3
^topic
Lifted values. #
^topicPlanned Feature
Deferred references. #
^topicPlanned Feature
Implicit lifetimes. #
^topicPlanned Feature
Reference lifetimes. #
^topicPlanned Feature
Shared ownership. #
^topicPlanned Feature
Deinitializer. #
^topicPlanned Feature
Pointer interop. #
^topicPlanned Feature
Automatic memory. #
Cyber uses an ARC/GC hybrid to automatically manage objects instantiated from object types. Value types typically do not need to be automatically managed unless they were lifted by a closure or a dynamic container.
^topicARC. #
ARC also known as automatic reference counting 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 a GC supplement when it is required.
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.
^topicObject destructor. #
An object's destructor invoked from ARC performs the following in order:
- Release child references thereby decrementing their reference counts by 1. If any child reference counts reach 0, their destructors are invoked.
- If the object has a finalizer, it's invoked.
- The object is freed from memory.
If the destructor is invoked by the GC instead of ARC, cyclable child references are not released in step 1. Since objects freed by the GC either belongs to a reference cycle or branched from one, the GC will still end up invoking the destructor of all unreachable objects. This implies that the destructor order is not reliable, but destructors are guaranteed to be invoked for all unreachable objects.
^topicRetain optimizations. #
When the lifetime of an object's reference is known on the stack, a large amount of retain/release ops can be avoided. For example, calling a function with an object doesn't need a retain since it is guaranteed to be alive when the function returns. This leaves only cases where an object must retain to ensure correctness such as escaping the stack.
When using dynamic types, the compiler can omit retain/release ops when it can infer the actual type even though they are dynamically typed to the user.
^topicClosures. #
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.
^topicFibers. #
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.
^topicHeap objects. #
Many object types are small enough to be at or under 40 bytes. To take advantage of this, object pools are reserved 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.
GC. #
The garbage collector is only used if the program may contain objects that form reference cycles. This property is statically determined by the compiler. Since ARC frees most objects, the GC's only responsibility is to free abandoned objects that form reference cycles. This reduces the amount of work for GC marking since only cyclable objects (objects that may contain a reference cycle) are considered.
Weak references are not supported for object types because objects are intended to behave like GC objects (the user should not be concerned with reference cycles). If weak references do get supported in the future, they will be introduced as a Weak[T]
type that is used with an explicit reference counted Rc[T]
type.
Currently, the GC can be manually invoked. However, the plan is for this to be automatic by either running in a separate thread or per virtual thread by running the GC incrementally.
To invoke the GC, call the builtin function: performGC
. Incomplete Feature: Only the main fiber stack is cleaned up at the moment.
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
Manual memory. #
^topicPlanned Feature
Memory allocations. #
^topicPlanned Feature
Runtime memory checks. #
Planned Feature When runtime memory checks are enabled, the compiler will insert traps and runtime logic to prevent the following unsafe uses of pointers and memory allocations:
- Use after free.
- Double free.
- Out of bounds memory access.
- Null pointer access.
- Misaligned pointer access.
- Unfreed memory.
With these checks in place, using manual memory will be much less error prone at the cost of some runtime performance and memory cost. Pointers will occupy an extra word size and the runtime will maintain an allocation table that contains the raw pointer and metadata.
^topicCLI. #
Basic commands. #
To compile and run a program with the VM, provide the path to the main Cyber source file:
cyber foo.cy
cyber path/to/main.cy
To see more options and commands, print the help screen:
cyber help
# These are aliases to the help command.
cyber -h
cyber --help
^topic
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.
C backend. #
The C backend generates a static binary from Cyber source code by first transpiling to C code and relying on a C compiler to produce the final executable.
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.