CX is a general purpose, interpreted and compiled programming language, with a very strict type system and a syntax similar to Golang's. CX provides a new programming paradigm based on the concept of affordances.
- Table of Contents
- CX Programming Language
- CX Roadmap
- Video Games in CX
- CX Playground
- Changelog
- Installation
- Additional Notes Before the Actual Installation
- Running CX
- Syntax
- Runtime
- Native Functions
- Type-inferenced Functions
- Slice Functions
- Input/Output Functions
- Parse Functions
- Unit Testing
- bool Type Functions
- str Type Functions
- i32 Type Functions
- i64 Type Functions
- f32 Type Functions
- f64 Type Functions
- time Package Functions
- os Package Functions
- gl Package Functions
- glfw Package Functions
- gltext Package Functions
CX is a general purpose, interpreted and compiled programming language, with a very strict type system and a syntax similar to Golang's. CX provides a new programming paradigm based on the concept of affordances, where the user can ask the programming language at runtime what can be done with a CX object (functions, expressions, packages, etc.), and interactively or automatically choose one of the affordances to be applied. This paradigm has the main objective of providing an additional security layer for decentralized, blockchain-based applications, but can also be used for general purpose programming.
In the following sections, the reader can find a short tutorial on how to use the main features of the language. In previous versions of this README file, the tutorial was written in a book-ish style and was targetted to a beginner audience. We're going to be making a transition from that style to a more technical style, without falling into a pure documentation style. The reason behind this is that a CX book is now going to be published, which is going to be targetted to a more beginner audience. Thus, this README now has the purpose of quickly demonstrating the capabilities of the language, how to install it, etc.
This tutorial/documentation is divided into four parts, which can broadly be described as introduction, syntax, runtime and native functions. The first section presents general information about the language, such as how to install it, the development roadmap, etc. The second and third sections are more tutorial-ish, where the reader can find information about the core language, i.e. how your program needs to look so it's considered a valid CX program (syntax), and how your CX program is going to be executing (runtime). The last section follows a more documentation style, where each of the native functions of the language is presented, along with an example.
Feel free to create an issue requesting a better explanation of a feature.
As mentioned in the description of the language, CX has a strict
type system. Most of the native functions in CX are associated to a
single type signature. For example, str.print
seen in the "Hello,
World!" program only accepts strings as its input argument. There are
versions of print
for each of the primitive types, such as
i32.print
, f32.print
, etc. The purpose of this is to
There are native functions in CX (the functions in the core language) associated to
In order to test out the language, programmers from the Skycoin community have already developed a number of video games, and more are in-coming. Below is a list of the current video games that we are aware of. If you are developing a video game using CX, please let us know in our official telegram for video game development https://t.me/skycoin_game_dev, or in the general CX group https://t.me/skycoin_cx.
- https://github.com/Lunier/Snake
- https://github.com/galah4d/casino-cx
- https://github.com/redcurse/SynthCat-Brick-Breaker
- https://github.com/atang152/crappyBall-cx
- https://github.com/galah4d/pacman-cx
If you want to test some CX examples, you can do it in the CX Playground (http://cx.skycoin.net/).
Check out the latest additions and bug fixes in the changelog.
This repository provides new binary releases of the language every week. Check this link and download the appropriate binary release for your platfrom: https://github.com/skycoin/cx/releases
More platforms will be added in the future.
CX has been successfully installed and tested in recent versions of Linux (Ubuntu), MacOS X and Windows. Nevertheless, if you run into any problems, please create an issue and we'll try to solve the problem as soon as possible.
Once you have downloaded and de-compressed the binary release file, you should place it somewhere in your operating system's $PATH environment variable (or similar). The purpose of this is to have cx globally accessible when using the terminal.
If you don't want to have it globally accessible, you can always try out CX locally, inside the directory where you have the binary file.
If a binary release is not currently available for your platfrom or if you want to have a nightly build of CX, you'll have to compile from source. If you're not familiarized with Go, Git, your OS's terminal or your OS's package manager (to name a few), we strongly recommend you to try out a binary release. If you find any bugs or problems with the binary release, submit an issue here: https://github.com/skycoin/cx/issues, and we'll fix it for the next week's release.
In order to compile CX from source, first make sure that you have Go
installed by running go version
. It should output something similar to:
go version go1.8.3 darwin/amd64
You need a version greater than 1.8, and >1.10 is recommended
Some linux distros' package managers install very old versions of Go. You can try first with a binary from your favorite package manager, but if the installation starts showing errors, try with the latest version before creating an issue.
Go should also be properly configured (you can read the installation instructions by clicking here. Particularly:
- Make sure that you have added the Go binary to your
$PATH
.- If you installed Go using a package manager, the Go binary is most
likely already in your
$PATH
variable. - If you already installed Go, but running "go" in a terminal throws a "command not found" error, this is most likely the problem.
- If you installed Go using a package manager, the Go binary is most
likely already in your
- Make sure that you have configured your
$GOPATH
environment variable. - Make sure that you have added
$GOPATH/bin
to you$PATH
.- If you have binaries installed in
$GOPATH/bin
but you can't use them by just typing their name wherever you are in the file system in a terminal, then this will solve the problem.
- If you have binaries installed in
As an example configuration, considering you're using bash in
Ubuntu, you would append to your ~/.bashrc
file this:
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin/
Don't just copy/paste that; think on what you're doing!
* Based on instructions from Viscript's repository.
CX comes with OpenGL and GLFW APIs. In order to use them, you need to install some dependencies. If you're using a Debian based Linux distro, such as Ubuntu, you can run these commands:
sudo apt-get install libxi-dev
sudo apt-get install libgl1-mesa-dev
sudo apt-get install libxrandr-dev
sudo apt-get install libxcursor-dev
sudo apt-get install libxinerama-dev
and you should be ready to go.
You might need to install GCC. Try installing everything first without installing GCC, and if an error similar to "gcc: command not found" is shown, you can fix this by installing MinGW.
Don't get GCC through Cygwin; apparently, Cygwin has compatibility issues with Go.
Users have reported that using either MingW or [tdm-gcc](tdm-gcc.tdragon.net(), where tdm-gcc seems to be the easiest way.
Make sure that you have curl
and git
installed. Run this command in a terminal:
sh <(curl -s https://raw.githubusercontent.com/skycoin/cx/master/cx.sh)
If you're skeptical about what this command does, you can check the
source code in this project. Basically, this script checks if you have
all the necessary Golang packages and tries to install them for
you. The script even downloads this repository and installs CX for
you. This means that you can run cx
after running the script and see
the REPL right away (if the script worked). To exit the REPL, you can press Ctrl-D
.
You should test your installation by running cx $GOPATH/src/github.com/skycoin/cx/tests
.
As an alternative, you could clone into this repository and run cx.sh in a terminal.
An installation script is also provided for Windows named cx-setup.bat
.
The Windows version of this method would be to manually
download the provided batch script (which is similar to the bash script for *nix systems described above), and run it in a terminal.
You should test your installation by running cx %GOPATH%\src\github.com\skycoin\cx\tests
.
Now you can update CX by simply running the installation script again:
./cx.sh
or, in Windows:
cx-setup.bat
Once CX has been successfully installed, running cx
should print
this in your terminal:
CX 0.5.13
More information about CX is available at http://cx.skycoin.net/ and https://github.com/skycoin/cx/
:func main {...
*
This is the CX REPL
(read-eval-print loop),
where you can debug and modify CX programs. The CX REPL starts with a
barebones CX structure (a main
package and a main
function) that
you can use to start building a program.
Let's create a small program to test the REPL. First, write
str.print("Testing the REPL")
after the *
, and press enter. After
pressing enter you'll see the message "Testing the REPL" on the screen. If you
then write :dp
(short for :dProgram
or debug program), you
should get the current program AST printed:
Program
0.- Package: main
Functions
0.- Function: main () ()
0.- Expression: str.print("" str)
1.- Function: *init () ()
As we can see, we have a main
package, a main
function, and we
have a single expression: str.print("Testing the REPL")
.
Let's now create a new function. In order to do this, we first need to
leave the main
function. At this moment, any expression (or function
call) that we add to our program is going to be added to main
. To
exit a function declaration, press Ctrl+D
. The prompt (*
) should
have changed indentation, and the REPL now shouldn't print :func main {...
above the prompt:
:func main {...
*
*
Now, let's enter a function prototype (an empty function which only specifies the name, the inputs and the outputs):
* func sum (num1 i32, num2 i32) (num3 i32) {}
*
You can check that the function was indeed added by issuing a :dp
command. If we want to add expressions to sum
, we have to select it:
* :func sum
:func sum {...
*
Notice that there's a semicolon before func sum
. Now we can add an expression to it:
:func sum {...
* num3 = num1 + num2
Now, exit sum
and select main
with the command :func main
. Let's
add a call to sum
and print the value that it returns when giving
the arguments 10 and 20:
:func main {...
* i32.print(sum(10, 20))
30
To run a CX program, you have to type, for example, cx the-program.cx
. Let's try to run some examples from the examples
directory in this repository. In a terminal, type this:
cd $GOPATH/src/github.com/skycoin/cx/
cx examples/hello-world.cx
This should print Hello World!
in the terminal. Now try running cx examples/opengl/game.cx
.
If you write cx --help
or cx -h
, you should see a text describing
CX's usage, options and more.
Some interesting options are:
--base
which generates a CX program's assembly code (in Go)--compile
which generates an executable file--repl
which loads the program and makes CX run in REPL mode (useful for debugging a program)--web
which starts CX as a RESTful web service (you can send code to be evaluated to this endpoint: http://127.0.0.1:5336/eval)
Do you want to know how CX looks? This is how you print "Hello, World!" in a terminal:
package main
func main () {
str.print("Hello, World!")
}
Every CX program must have at least a main package, and a main function. As mentioned before, CX has a strict type system, where functions can only be associated with a single type signature. As a consequence, if we want to print a string, as in the example above, we have to call str's print function, where str is a package containing string related functions.
However, there are some exceptions, mainly to functions where it makes
sense to have a generalized type signature. For example, the len
function accepts arrays of any type as its argument, instead of having
[]i32.len()
or [][]str.len()
. Another example is sprintf
, which
is used to construct a string using a format string and a series of
arguments of any type.
In this section, we're going to have a look at how a CX program looks like. Basically, the following sections are not going to discuss about the logic behind the various CX constructs, i.e. how they behave; we're only going to see how they look like.
Some of the code snippets that follow have comments in them, i.e., blocks of text that are not actually "run" by the CX compiler or interpreter. Just like in C, Golang and many other programming languages, single line comments are created by placing double slashes (//) before the text being commented. For example:
// Example of adding two 32 bit integers in CX
i32.add(3, 4) // This will be ignored
// End of the program
Mult-line comments are opened by writing slash-asterisk (/*), and are closed by writing asterisk-slash (*/).
/* This code won't be executed
str.print("Hello world!")
*/
A declaration refers to a named element in a program's structure, which are described using other constructs, such as expressions and other statements. For example: a function can be referred by its name and it's constructed by expressions and local variable declarations.
Any name that satisfies the PCRE regular expression
[_a-zA-Z][_a-zA-Z0-9]*
is allowed as an identifier for a declared
element. In other words, an identifier can start with an underscore
(_) or any lowercase or uppercase letter, and can be followed by 0
or more underscores or lowercase or uppercase letters, and any number
from 0 to 9.
One of CX's goals is to provide a very strict type system. The purpose
of this is to reduce runtime errors as much as possible. In order to
achieve this goal, many of CX's native functions are constrained to a
single type signature. For example, if you want to add two 32-bit
integers, you'd need to use i32.add
. In contrast, if you want to add
two 64-bit integers, you'd use i64.add
. These functions can help the
programmer to ensure that a particular data type is being received or
sent during a process.
If the programmer doesn't want to use those type-specific functions,
CX still provides type inference in some cases. For example, instead
of writing i32.add(5, 5)
you can just write 5 + 5
. In this case,
CX is going to see that you're using 32-bit integers, and the parser
is going to transform that expression to i32.add(5, 5)
. However, if
you try to do 5 + 5L
, i.e. if you try to add a 32-bit integer to a
64-bit integer, CX will throw a compile-time error because you're
mixing types.
The proper way to handle types in CX is to explicitly parse
everything. This way one can be sure that you're always going to be
handling the desired type. So, retaking the previous example, you'd
need to parse one of them to match the other's type, either
i32.i64(5) + 5L
or 5 + i64.i32(5L)
.
There are seven primitive types in CX: bool, str, byte, i32, i64, f32, and f64. Those represent Booleans (true or false), character strings, bytes, 32-bit integers, 64-bit integers, single precision and double precision floating-point numbers, respectively.
Global variables are different from local variables regarding scope. Global variables are available to any function defined in a package, and to any package that is importing the package that contains that global declaration. An example of some global variables is shown below.
package main
var global1 i32
var global2 i64
func foo () {
i32.print(global1)
i64.print(global2)
}
func main () {
global1 = 5
global2 = 5L
i32.print(global1)
i64.print(global2)
}
In the example above we can see that both the main
and foo
functions are printing the values of the two global variables
defined. They are going to print the same values, as they are
referring to the same variables.
In contrast to global variables, local variables are constrained to the function where they are declared. This means that is not possible for another function to call a variable defined in another function.
package main
func foo () {
i32.print(local) // this expression will throw a compile-time error
}
func main () {
var local i32
local = 5
i32.print(5)
foo()
}
If you try to run the example above, CX will throw an error similar to
this: error: examples/testing.cx:4 identifier 'local' does not exist
, so CX will not even try to run that program. If we could
de-activate CX's compile-time type checking, and the program above
could make it to the runtime, CX would not print 5 when running
foo()
, as that function is unaware of that variable.
Arrays (or vectors) and multi-dimensional arrays (or matrices) can be declared using a syntax similar to C's.
package main
type Point struct {
x i32
y i32
}
func main () {
var arr1 [5]i32
var arr2 [5]Point
var arr3 [2][2]f32
arr1[0] = 10
arr2[1] = 20
}
In the example above we see the declaration of an array of 5 elements of type i32, followed by an array of the same cardinality but of type Point, which is a custom type. Custom types are discussed in a later section. Lastly, we see an example of a 2x2 matrix of type f32.
Lastly, we can see how we can initialize an array using the bracket
notation, e.g. arr1[0] = 10
.
Golang-like slices exist in CX (dynamic arrays). Slices are declared similarly to arrays, with the only difference that the size is omitted.
package main
type Point struct {
x i32
y i32
}
func main () {
var slc1 []i32
var slc2 []Point
var slc3 [][]f32
}
Slices, unlike arrays, cannot be directly initialized using the
bracket notation (unless you use the native function make
first). You can use the bracket notation to reassign values to a
slice, once an element associated to the index that you want to use
already exists, as shown in the example below.
package main
func main () {
var slc []i32
slc = append(slc, 1)
slc = append(slc, 2)
slc[0] = 10
slc[2] = 30 // This is not allowed, as len(slc) == 2, not 3
}
As this behavior is more related to the logic behind slices, it is further explained in the Runtime->Data Structures->Slices section.
A literal is any data structure that is not being referenced by any
variable yet. For example: 1
, true
, []i32{1, 2, 3}
, Point{x: 10, y: 20}
.
Particularly, it is worth noting the cases of array, slice and struct literals.
package main
type Point struct {
x i32
y i32
}
func main () {
var a Point
var b [5]i32
var c []i32
a = Point{x: 10, y: 20}
b = [5]i32{1, 2, 3, 4, 5}
c = []i32{100, 200, 300}
}
In the example above we can see examples of struct (Point{x: 10, y: 20}
), array ([5]i32{1, 2, 3, 4, 5}
), and slice ([]i32{100, 200, 300}
)
literals, in that order. These literals exist to simplify the creation
of such data structures.
Functions in CX are similar in syntax to functions in Go. The only exception is that named outputs are enforced at the moment (this will most likely change in the future).
package main
func foo () {
}
func main () {
foo()
}
The example above doesn't do anything, but it illustrates the anatomy
of a function. In the case of foo
, we have an empty function
declaration, and then we have main
, which is defined by a single
call to foo
. Functions can also receive inputs and return outputs,
as in the example below.
package main
func foo (in i32) {
i32.print(in) // this will print 5
}
func bar () (out i32){
out = 10
}
func main () {
foo(5)
var local i32
local = bar()
i32.print(local) // this will print 10
}
In this case, foo
is declared to receive one input parameter, and
bar
is declared to return one output parameter.
If primitive types are not enough, you can define your own custom types by combining the primitive types and other constructs like slices, arrays, and even other custom types.
package main
type Point struct {
x i32
y i32
}
func main () {
var p Point
p.x = 10
p.y = 20
printf("Point coordinates: (%d, %d)\n", p.x, p.y)
}
In the example above, we can see a custom type that defines a
Point as the combination of two 32-bit integers (i32). After
declaring the custom type, you can start declaring variables of that
type anywhere in the package where it was declared in. The code in
foo
shows how you can create and use an instance of that structure.
A variation of functions that are associated to custom types are methods.
package main
type Point struct {
x i32
y i32
}
type Line struct {
a Point
b Point
}
func (p Point) print () {
printf("Point coordinates: (%d, %d)\n", p.x, p.y)
}
func (l Line) print () {
printf("Line point A: (%d, %d), Line point B: (%d, %d)\n", l.a.x, l.a.y, l.b.x, l.b.y)
}
func main () {
var l Line
var p1 Point
var p2 Point
p1.x = 10
p1.y = 20
p2.x = 11
p2.y = 21
l.a = p1
l.b = p2
p1.print()
p2.print()
l.print()
}
In the example above, we define two custom types: Point and Line. The type line is defined by two fields of type Point, and the type Point is defined as coordinate defined by two fields of type i32.
As a simple example, we create two methods called print
, one for the
type Point and another for the type Line. In the case of
Point.print
, we just print the two coordinates, and in
the case of Line.print
we print the coordinates of the two points
that define the Line instance.
In the previous examples we have always been using a single package:
main
. If your program grows too large it's convenient to divide your
code into different packages.
package foo
func fn (in i32) {
i32.print(in)
}
package bar
func fn () (out i32) {
out = 5
}
package main
import "foo"
import "bar"
func main () {
foo.fn(10) // prints 10
var num i32
num = bar.fn()
i32.print(num) // prints
}
In the example above, we can see how two functions with the same name
(fn
) are declared, each in a separate package. Both of these
functions have different signatures, as foo.fn
accepts a single
input parameter and bar.fn
doesn't accept any inputs but returns a
single output parameter.
We can then see how the main
package import
s both the foo
and
bar
packages, to later call each of these functions.
Statements are different to declarations, as they don't create any named elements in a program. They are used to control the flow of a program.
The most basic statement is the if statement, which is going to execute a block of code only if a condition is true.
package main
func main () {
if false {
str.print("This will never print")
}
}
The example above won't do anything, as the condition for the if statement is always going to evaluate to false.
package main
func main () {
if true {
str.print("This will always print")
}
}
In contrast, the example above will always print.
package main
func main () {
if true {
str.print("This will always print")
} else {
str.print("This will never print")
}
}
Lastly, the example above shows how to write an if/else statement in CX.
As a note about its syntax, the predicates or conditions don't need to be enclosed in parentheses, just like in Go.
CX's only looping statement is the for loop. Similar to Go, the for loop in CX can be used as the while statement in other programming languages, and as a traditional for statement.
package main
func main () {
for true {
str.print("forever")
}
}
As the simplest example of a loop, we have the infinite loop shown in
the example above. In this case, the loop will print the character
string "forever" indefinitely. If you try this code, remember that you
can cancel the program's execution by hitting Ctrl-C
.
package main
func main () {
for str.eq("hi", "hi") {
str.print("hi")
}
}
The code above shows another example, one where we use an expression
as its predicate, rather than a literal true
or false
. It is worth
mentioning that you can replace str.eq("hi", "hi")
by "hi" == "hi"
.
package main
func main () {
var c i32
for c = 0 ; c < 10; c++ {
i32.print(c)
}
}
The traditional for loop shown in the example above. In languages like C, you need to first declare your counter variable, and then you have the option to initialize or reassign the counter in the first part of the for loop. The second part of the for loop is reserved for the predicate, and the last part is usually used to increment the counter. Nevertheless, just like in C, there's nothing stopping you from doing whatever you want in the first and last parts. However, the predicate part needs to be an expression that evaluates to a Boolean.
package main
func main () {
for c := 0; c < 10; c++ {
i32.print(c)
}
}
A more Go-ish way of declaring and initializing the counter is to use an inline declaration, as seen in the example above.
package main
func main () {
var c i32
c = 0
for ; c < 10; c++ {
i32.print(c)
}
}
Lastly, the for loop can also completely omit the initialization part, as seen above.
goto
can be used to immediately jump the execution of a program to
the corresponding labeled expression.
package main
func main () (out i32) {
goto label2
label1:
str.print("this should never be reached")
label2:
str.print("this should be printed")
}
In the example above, we see how a goto
statement forces CX to
ignore executing the expression labeled as label1
, and instead jumps
to the label2
expression.
Expressions are basically function calls. But the term expression also takes into consideration the variables that are receiving the function's output arguments, the input arguments, and any dereference operations.
package main
func foo () (arr [2]i32) {
arr = [2]i32{10, 20}
}
func main () {
i32.print(foo()[0])
}
For example, the expression i32.print(foo()[0])
in the code above
consists of two function calls, and the array returned by the call
to foo
is "dereferenced" to its 0th element.
As in many other C-like languages, assignments are done using the
equal (=
) sign.
package main
func main () {
var foo i32
foo = 5
foo = 50
}
In the case of the code above, the variable foo
is declared and then
initialized to 5
using the =
sign. Then we reassign the foo
variable to the value 50
.
package main
func main () {
foo := 5
foo = 50
}
As in other programming languages, short variable declarations exist
in CX. The :=
token can be used to tell CX to infer a variable's
type. This way, CX declares and initializes at the same time, as seen
in the example above.
The affordance system in CX uses a special operator: ->
. This
operator takes a series of statements that have the form of function
calls, and transforms them to a series of instructions that can be
internally interpreted by the affordance system.
package main
func exprPredicate (expr Expression) (res bool) {
if expr.Operator == "i32.add" {
res = true
}
}
func prgrmPredicate (prgrm Program) () {
if prgrm.FreeHeap > 50 {
res = true
}
}
func main () {
num1 := 5
num2 := 10
targetExpr:
sum := i32.add(0, 0)
tgt := ->{
pkg(main)
fn(main)
expr(targetExpr)
}
fltrs := ->{
filter(exprPredicate)
filter(prgrmPredicate)
}
aff.print(tgt)
aff.print(fltrs)
affs := aff.query(fltrs, tgt)
aff.on(affs, tgt)
aff.of(affs, tgt)
aff.inform(affs, 0, tgt)
aff.request(affs, 0, tgt)
}
The previous section presents the language features from a syntax perspective. In this section we'll cover what's the logic behind these features: how they interact with other elements in your program, and what are the intrinsic capabilities of each of these features.
Packages are CX's mechanism for better organizing your code. Although it is theoretically possible to store a big project in a single package, the code will most likely become very hard to understand. In CX the programmer is encouraged to place the files that define the code of a package in separate directory. Any subdirectory in a package's directory should also contain only source code files that define elements of the same package. Nevertheless, CX will not throw any error if you don't follow this way of laying out your source files. In fact, you can declare different packages in a single source code file.
Data structures are particular arrangements of bytes that the language interprets and stores in special ways. The most basic data structures represent basic data, such as numbers and character strings, but these basic types can be used to construct more complex data types.
A literal is any data structure that is not being referenced by any
variable yet. For example: 1
, true
, []i32{1, 2, 3}
, Point{x: 10, y: 20}
.
It's important to make a distinction, particularly with arrays, slices and struct instances.
package main
type Point struct {
x i32
y i32
}
func main () {
var p1 Point
p1.x = 10
p1.y = 20
p2 := Point{x: 11, y: 21}
i32.print(p2.x)
i32.print(p2.y)
}
In the example above we are creating two instances of the Point
type. The first method we use does not involve struct literals, as a
variable of that type is first created and then initialized.
In the second case (p2
), the full struct instance is first
created. CX creates an anonymous struct instance as soon as it
encounters Points{x: 11, y: 21}
, and then it proceeds to assign that
literal to the p2
variable, using short variable declarations.
package main
func main () {
var arr1 [3]i32
arr1[0] = 1
arr1[1] = 2
arr1[2] = 3
arr2 := [3]i32{10, 20, 30}
}
package main
func main () {
var slc1 []i32
slc1 = append(slc1, 1)
slc1 = append(slc1, 2)
slc1 = append(slc1, 3)
slc2 := []i32{10, 20, 30}
}
Similarly, in the two examples above we can see how we can declare
array and slice variables and then we initialize them. In the case of
arrays, we use the bracket notation, and for slices we have to use
append
, as slc1
starts with a size and capacity of 0. In the cases
of arr2
and slc2
, we use literals to initialize them more
conveniently.
Regarding numbers, you need to be aware that implicit casting does not
exist in CX. This means that the number 34
cannot be assigned to a
variable of type i64. In order to assign it, you need to either
parse it using the native function i32.i64
or you can create a
64-bit integer literal. To create a number literal of a type other
than i32, you can use different suffixes: B
, L
and D
, for
byte, i64 (long) and f64, respectively. So, assuming foo
is of
type i64, you can do this assignment: foo = 34L
.
When CX compiles a program, it knows how many bytes need to be reserved in the stack for each of the functions. CX can know this thanks to variable declarations.
package main
type Point struct {
x i32
y i32
}
func foo (inp Point) {
var test1 i64
var test2 bool
}
func main () {
var test3 i32
var test4 f32
}
The two functions declared in the example above are going to reserve 17
and 8 bytes in the stack, respectively. In the case of the first
function, foo
needs to reserve space for an input parameter of type
Point
, which requires 8 bytes (because of the two i32 fields), and
two local variables: one 64-bit integer that requires 8 bytes and a
Boolean that requires a single byte. In the case of main
, CX needs
to reserve bytes for two local variables: a 32-bit integer and a
single-precision floating point number, where each of them require 4
bytes.
package main
var global1 i32
func main () {
var local i32
}
Local variables are different than global variables. In order for globals to have a global scope they need to be allocated in a different memory segment than local variables. This different memory segment does not shrink or get bigger like the stack. This means that any global variable is going to be kept "alive" as long as the program keeps being executed.
A global scope means that variables of this type are accessible to any function declared in the same package where the variable is declared, and to any function of other packages that are importing this package.
package main
func main () {
var foo i32
i32.print(foo) // prints 0
}
In CX every variable is going to initially point to a nil
value. This nil value is basically a series of one or more zeroes,
depending on the size of the data type of a given variable. For
example, in the code above we see that we have declared a variable of
type i32 and we immediately print its value without initializing
it. This CX program will print 0, as the value of foo
is [0 0 0 0]
in the stack (4 zeroes, as a 32-bit integer is represented by 4
bytes).
In the case of data types that point to variable-sized structures, such as slices or character strings, these are initialized to a nil pointer, which is represented by 4 zeroed bytes. This nil pointer is located in the heap memory segment, instead of the stack.
There are seven primitive types in CX: bool, str, byte, i32, i64, f32, and f64. These types can be used to construct other more complex types, as will be seen in the next sections.
bool and byte both require a single byte to represent their
values. In the case of bool, there are only two possible values:
true
or false
. In the case of byte you can represent up to 256
values, which range from 0 to 255. Next in size, we have i32 and
f32 , where both of them require 4 bytes, and then we have i64
and f64, which require 8 bytes each.
Now, strings are special as they are static and dynamic sized at the same time. If you have a look at how a variable of type str reserves memory in the stack, you'll see that it requires 4 bytes, regardless of what text it's pointing to. The explanation behind this is that any str in CX actually behaves like a pointer behind the scenes, and the actual string gets stored in the heap memory segment.
package main
func main () {
var foo str
foo = str.concat("Hello, ", "World!")
foo = "Hi"
}
When CX compiles the example above, three strings are first stored in
the data memory segment (just like global variables, as these strings
are constants, memory-wise): "Hello, "
, "World"
and "Hi"
. When
the program is executed, str.concat
is called, which creates a new
string by concatenating "Hello, "
and "World!"
, and this new
character string is allocated in the heap memory segment. Then foo
is assigned only the address of this new character string. Then we
immediately re-assign foo
with the address of "Hi"
. This means
that foo
was first assigned a memory address located in the data
memory segment, and then it was assigned an address located in the
heap.
Arrays, as in other programming languages, are used to create collections of data structures. These data structures can be primitive types, custom types or even arrays or slices.
package main
type Point struct {
x i32
y i32
}
func main () {
var [5]i32
var [5]Point
}
In the example above, we're creating two arrays, one of a primitive
type and the other one of a custom type. CX reserves memory for these
arrays in the stack as soon as the function that contains them is
called. In this case, 60 bytes are going to be reserved for main
as
soon as the program starts its execution, as main
acts as the
program's entry point. You need to be careful with arrays, as those
can easily fill up your memory, especially with multi-dimensional
arrays (or matrices).
Also, another point to consider is performance. While accessing arrays is almost as fast as accessing an atomic variable, arrays can be troublesome when being sent/received as to/from functions. The reason behind this is that an array needs to be copied whenever it is sent to another function. If you're working with arrays of millions of elements and you need to be sending that arrays millions of times to another function, it's going to impact your program's performance a lot. A way to avoid this is to either use pointers to arrays or slices.
Dynamic arrays don't exist in CX. This means that the following code is not a valid CX program:
package main
func main () {
var size i32
size = 13
var arr [size] // this is not valid
}
If you need an array that can grow in size as required, you need to use slices. Behind the scenes, slices are just arrays with some extra features. First of all, any slice in CX goes directly to the heap, as it's a data structure that is going to be changing in size. In contrast, arrays are always going to be stored in the stack, unless we're handling pointers to arrays. However, this behavior may change in the future, when CX's escape analysis mechanism improves (for example, the compiler can determine if an array is never going to change its size, and decide to keep it in the stack).
The second characteristic of slices in CX is how they change their size. Any slice, when it's first declared, starts with a size and capacity of 0. The size represents how many elements are in a given slice, while the capacity represents how many elements can be allocated in that slice without having to be relocated in the heap.
package main
func main () {
var slc []i32
slc = append(slc, 1)
slc = append(slc, 2)
slc = append(slc, 3)
slc = append(slc, 4)
}
In the code above we can see how we declare a slice and then we
initialize it using the append
function. After all the append
s,
we'll end up with a slice of size 4 and capacity 4, and this
append
ing process will create the following objects in the heap:
[0 0 0 0 0 12 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 16 0 0 0 2 0 0 0 2 0 0 0 1 0 0 0 2 0 0 0 0 0 0 0 0 24 0 0 0 4 0 0 0 4 0 0 0 1 0 0 0 2 0 0 0 3 0 0 0 4 0 0 0]
First, the slice slc
starts with 0 objects in it; it is pointing to
nil. Then, after the first append
, the object
[0 0 0 0 0 12 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0]
is allocated to the
heap. The first five bytes are used by CX's garbage collector. The
next 4 bytes indicate the size of the object, and the remaining bytes
are the actual slice slc
. The first four bytes of slc
tell us its
current size, while the next four tell us its capacity. The remaining
bytes of this object are the elements of the slice.
The following object,
[0 0 0 0 0 16 0 0 0 2 0 0 0 2 0 0 0 1 0 0 0 2 0 0 0]
, shows now a
size of 2 and a capacity of 2, with the 32-bit integers 1
and 2
as
its elements. The last object, 0 0 0 0 0 24 0 0 0 4 0 0 0 4 0 0 0 1 0 0 0 2 0 0 0 3 0 0 0 4 0 0 0
, needs careful attention. We can see that
our objects jumped from size 1 to 2 and finally 4. The same happened
to its capacity, and the containing elements are now 1
, 2
, 3
and
4
. What happened to the slice of size 3 and capacity 3? First of
all, capacities are increased by getting doubled each time the size of
an object is greater than its capacity, so we would never get a slice
of capacity 4 by following this method. Next, we need to think on what
is capacity used for.
Slices are just arrays, which means that they can't be resized. The dynamic nature of slices is emulated by copying the full slice to somewhere else in memory, but with a greater capacity. However, this will only happen if adding a new element to the existing slice would overflow it. This is why slices keep track of two metrics: size and capacity, i.e. how many actual elements are in the slice, and how many elements the currently allocated slice can hold, respectively.
package main
func main () {
var arr1 [1]i32
arr1[0] = 1 // add the first value
var arr2 [2]i32 // double the size
arr2[0] = arr1[0] // copy previous array
arr2[1] = 2 // add the second value
var arr3 [4]i32 // double the size
arr3[0] = arr2[0] // copy previous array
arr3[1] = arr2[1] // copy previous array
arr3[2] = 3 // add the third value
arr3[3] = 4 // add the fourth value
}
The example above shows the behavior of the slice in the previous example, but using arrays.
Structures are CX's mechanism for creating custom types, as in many other
C-like languages. Structures are basically a grouping of other
primitive or custom types (called fields) that together create
another type of data structure. For example, a point can be defined
by its coordinates in a two-dimensional space. In order to create a
type Point
, you can use a structure that contains two fields of type
i32, one for x
and another for y
, as in the example below.
package main
type Point struct {
x i32
y i32
}
func main () {
var p Point
p.x = 10
p.y = 20
}
Whenever an instance of a structure is created by either declaring a variable of that type or by creating a literal of that type, CX reserves memory to hold space for all the fields defined in the structure declaration. Like in C, the bytes are reserved depending on the order of the fields in the structure declaration.
package main
type struct1 struct {
field1 bool
field2 i32
field3 i64
}
type struct2 struct {
field1 i64
field2 bool
field3 i32
}
func main () {
var s1 struct1
var s2 struct2
}
For example, in the code above a call to main
will reserve a total
of 26 bytes in the stack. In the case of the first struct instance,
the first byte is going to represent field1
of type bool, the next
four bytes are going to represent field2
of type i32, and the
final 8 bytes are going to represent field3
of type i64. In the
case of the next struct instance, the first eight bytes represent an
i64 field so, although both struct instances contain the same number
of fields and of the same type, the byte layout changes.
Sometimes it's useful to pass variables to functions by reference instead of by value.
package main
import "time"
func foo (nums [100][100]i32) {
// do something with nums
}
func main () {
var start i64
var end i64
var nums [100][100]i32
start = time.UnixMilli()
for c := 0; c < 10000; c++ {
foo(nums)
}
end = time.UnixMilli()
printf("elapsed time: \t%d milliseconds\n", end - start)
}
The example above is very inefficient, as CX is going to be sending a
10,000 element matrix to foo
10,000 times. Every time foo
is
called, every byte of that matrix needs to be copied for foo
. In my
computer the example above takes around 638 milliseconds to run.
package main
import "time"
func foo (nums *[100][100]i32) {
// do something with nums
}
func main () {
var start i64
var end i64
var nums [100][100]i32
start = time.UnixMilli()
for c := 0; c < 10000; c++ {
foo(&nums)
}
end = time.UnixMilli()
printf("elapsed time: \t%d milliseconds\n", end - start)
}
A new version of the last program is shown above. In contrast to the
last program, the code above sends a pointer to the matrix to
foo
. A pointer in CX uses only 4 bytes (in the future, pointers will
use 8 bytes in 64-bit systems and 4 bytes in 32-bit systems), so
instead of copying 10,000 bytes, we only copy 4 bytes to foo
every
time we call it. This version of the program takes only 3 milliseconds
to run in my computer.
package main
func foo (inp i32) {
inp = 10
}
func main () {
var num i32
num = 15
i32.print(num) // prints 15
foo(num)
i32.print(num) // prints 15
}
In the example above, we sendnum
to foo
, and then we re-assign
the input's value to 10
. If we print the value of num
before and
after calling foo
, we can see that in both instances 15
will be
printed to the console.
package main
func foo (num *i32) {
*num = 10
}
func main () {
var num i32
num = 15
i32.print(num) // prints 15
foo(&num)
i32.print(num) // prints 10
}
The code above is a pointer-version of the previous example. In this
case, instead of sending num
by value, we send it by reference,
using the &
operator. foo
also changed, and it now accepts a
pointer to a 32-bit integer, i.e. *i32
. After running the example,
you'll notice that, this time, foo
is now changing num
's value.
Consider the following example:
package main
func foo () (pNum *i32) {
var num i32
num = 5 // this is in the stack
pNum = &num
}
func stackDestroyer () {
var arr [5]i32
}
func main () {
var pNum *i32
pNum = foo()
stackDestroyer()
i32.print(*pNum)
}
If we store foo
's num
's value (5
) in the stack, and then we call
stackDestroyer
, isn't arr
going to overwrite the bytes storing the
5
? This doesn't happen, because that 5
is now in the heap. But
this doesn't mean that any value being pointed to is going to be moved
to the heap. For example, let's re-examine one of the examples
presented in the Pointers section:
package main
func foo (num *i32) {
*num = 10
}
func main () {
var num i32
num = 15
i32.print(num) // prints 15
foo(&num)
i32.print(num) // prints 10
}
If any value being pointed to by a pointer was sent to the heap, we
wouldn't be able to change num
s value, which is stored in the stack;
we would be changing the heap's copied value.
package main
func foo () (pNum *i32) {
var num i32
var pNum *i32
num = 5
pNum = &num
}
func main () {
var pNum *i32
pNum = foo()
i32.print(*pNum) // prints 5, which is stored in the heap
}
Basically, in order to fix this problem, whenever a pointer needs to
be returned from a function, the value it is pointing to "escapes" to
the heap. In the example above, we can see that num
's value is going
to be preserved by escaping to the heap, as we are returning a pointer
to it from foo
.
package main
func foo () (pNum *i32) {
var num i32
var pNum *i32
num = 5 // this is in the stack
pNum = &num // the pointer will be returned, so the value is sent to the heap
}
func stackDestroyer () {
var arr [5]i32
}
func main () {
var pNum *i32
pNum = foo()
stackDestroyer() // if 5 does not escape, it would be destroyed by this function
i32.print(*pNum) // prints 5, which is stored in the heap
}
We can check this behavior even further in the example above. After
calling foo
, we call stackDestroyer
, which overwrites the
following 20 bytes after main
's stack frame. Yet, when we call
i32.print(*pNum)
, we'll see that we still have access to a 5
. This
5
is not the one created in foo
, though, but a copy of it that was
allocated in the heap.
Once we have the appropriate data structures for our program, we'll now need to process them. In order to do so, we need to have access to some control flow structures.
Functions are used to encapsulate routines that we plan to be frequently calling. In addition to encapsulating a series of expressions and statements, we can also receive input parameters and return output parameters, just like mathematical functions.
package main
func main () {
var players []str
players = []str{"Richard", "Mario", "Edward"}
str.print("=======================")
str.print(str.concat("Name: \t", players[0]))
str.print("=======================")
str.print("=======================")
str.print(str.concat("Name: \t", players[1]))
str.print("=======================")
str.print("=======================")
str.print(str.concat("Name: \t", players[2]))
str.print("=======================")
}
For example, if we see the code above we'll notice that it seems repetitive. We can fix this by creating a function, as seen in the example below.
package main
func drawBox (player str) {
str.print("=======================")
str.print(str.concat("Name: \t", player))
str.print("=======================")
}
func main () {
var players []str
players = []str{"Richard", "Mario", "Edward"}
drawBox(players[0])
drawBox(players[1])
drawBox(players[2])
}
Methods are useful when we want to associate a particular function to a particular custom type (associating functions to primitive types is not allowed). This allows us to create more readable code.
package main
type Player struct {
Name str
HP i32
Mana i32
Lives i32
}
type Monster struct {
Name str
HP i32
Mana i32
}
func (player Player) draw () {
str.print(sprintf("\n\tName: \t%s\n\tHP: \t%d\n\tMana: \t%d\n\tLives: \t%d\n\n%s",
player.Name,
player.HP,
player.Mana,
player.Lives,
`
─▄████▄▄░
▄▀█▀▐└─┐░░
█▄▐▌▄█▄┘██
└▄▄▄▄▄┘███
██▒█▒███▀`))
}
func (monster Monster) draw () {
str.print(sprintf("\n\tName: \t%s\n\tHP: \t%d\n\tMana: \t%d\n\n%s",
monster.Name,
monster.HP,
monster.Mana,
`
╲╲╭━━━━╮╲╲
╭╮┃▆┈┈▆┃╭╮
┃╰┫▽▽▽▽┣╯┃
╰━┫△△△△┣━╯
╲╲┃┈┈┈┈┃╲╲
╲╲┃┈┏┓┈┃╲╲
▔▔╰━╯╰━╯▔▔`))
}
func main () {
var player Player
player.Name = "Mario"
player.HP = 10
player.Mana = 10
player.Lives = 3
player.draw()
var monster Monster
monster.Name = "Domo-kun"
monster.HP = 7
monster.Mana = 4
monster.draw()
}
The example above shows us how we can create two versions of the
function draw
, and the behavior of each depends on the custom type
that we're using to call it.
if and if/else statements are used to execute a block of
instructions only if certain condition is true or false. Behind the
scenes, if and if/else statements are parsed to a series of
jmp
instructions internally. For example, in the case of an if
statement, we will jump 0 instructions if certain predicate is
true, and it will jump n instructions if the predicate is false,
where n is the number of instructions in the if block of
instructions.
package main
func main () {
if true {
str.print("hi")
}
str.print("bye")
}
Program
0.- Package: main
Functions
0.- Function: main () ()
0.- Expression: jmp(true bool)
1.- Expression: str.print("" str)
2.- Expression: jmp(true bool)
3.- Expression: str.print("" str)
1.- Function: *init () ()
In the two code snippets above we can see how an if statement is
translated by the parser to a set of two jmp
instructions. These
jmp
instructions have some meta data in them that is not shown in
the second snippet: how many lines to jump if its predicate is true
and how many lines to jump if the predicate is false. jmp
is
not meant to be used by CX programmers (it's only part of the CX base
language), so you don't need to worry about it.
package main
type Player struct {
Name str
HP i32
Mana i32
Lives i32
}
type Monster struct {
Name str
HP i32
Mana i32
}
func main () {
var player Player
player.Name = "Mario"
player.HP = 10
player.Mana = 10
player.Lives = 3
var monster Monster
monster.Name = "Domo-kun"
monster.HP = 7
monster.Mana = 4
if player.HP < 5 {
str.print("===DANGER!===")
} else {
str.print("===YOU CAN DO IT!===")
}
if monster.HP < 10 {
str.print(sprintf("===%s is bleeding!===", monster.Name))
}
if monster.HP < 5 {
str.print(sprintf("===%s is dying!===", monster.Name))
}
if monster.HP == 0 {
str.print(sprintf("===%s is dead!===", monster.Name))
}
}
Continuing with the example from the previous section (to some extent), let's use if and if/else statements to determine what messages are going to be displayed to the user. These messages represent the state of the player or the monster, depending on their hit points (HP).
The for loop is the only looping mechanism in CX. Just like if and
if/else statements are constructed using jmp
statements, for
loop statements are also constructed the same way.
package main
func main () {
for c := 0; c < 10; c++ {
i32.print(c)
}
}
Program
0.- Package: main
Functions
0.- Function: main () ()
0.- Declaration: c i32
1.- Expression: c i32 = identity(0 i32)
2.- Expression: *lcl_0 bool = lt(c i32, 10 i32)
3.- Expression: jmp(*lcl_0 bool)
4.- Expression: i32.print(c i32)
5.- Declaration: c i32
6.- Expression: c i32 = i32.add(c i32, 1 i32)
7.- Expression: jmp(true bool)
1.- Function: *init () ()
The code snippets above illustrate how a for loop that counts from 0
to 9 is translated to a set of of jmp
instructions.
package main
type Player struct {
Name str
HP i32
Mana i32
Lives i32
}
type Monster struct {
Name str
HP i32
Mana i32
}
func (player Player) draw () {
str.print(sprintf("\n\tName: \t%s\n\tHP: \t%d\n\tMana: \t%d\n\tLives: \t%d\n\n%s",
player.Name,
player.HP,
player.Mana,
player.Lives,
`
─▄████▄▄░
▄▀█▀▐└─┐░░
█▄▐▌▄█▄┘██
└▄▄▄▄▄┘███
██▒█▒███▀`))
}
func (monster Monster) draw () {
str.print(sprintf("\n\tName: \t%s\n\tHP: \t%d\n\tMana: \t%d\n\n%s",
monster.Name,
monster.HP,
monster.Mana,
`
╲╲╭━━━━╮╲╲
╭╮┃▆┈┈▆┃╭╮
┃╰┫▽▽▽▽┣╯┃
╰━┫△△△△┣━╯
╲╲┃┈┈┈┈┃╲╲
╲╲┃┈┏┓┈┃╲╲
▔▔╰━╯╰━╯▔▔`))
}
func (player Player) attack (cmd str, monster *Monster) {
if bool.or(cmd == "M", cmd == "m") {
var dmg i32
dmg = i32.rand(1, 4)
(*monster).HP = (*monster).HP - dmg
printf("'%s' suffered a magic attack. Lost %d HP. New HP is %d\n", (*monster).Name, dmg, (*monster).HP)
} else {
var dmg i32
dmg = i32.rand(1, 2)
(*monster).HP = (*monster).HP - dmg
printf("'%s' suffered a physical attack. Lost %d HP. New HP is %d\n", (*monster).Name, dmg, (*monster).HP)
}
}
func (monster Monster) attack (cmd str, player *Player) {
var dmg i32
dmg = i32.rand(1, 5)
(*player).HP = (*player).HP - dmg
printf("'%s' suffered a physical attack. Lost %d HP. New HP is %d\n", (*player).Name, dmg, (*player).HP)
}
func battleStatus (player Player, monster Monster) {
if player.HP < 5 {
str.print("===DANGER!===")
} else {
str.print("===YOU CAN DO IT!===")
}
if player.HP == 0 {
str.print("===YOU DIED===")
}
if monster.HP < 10 && monster.HP >= 5 {
str.print(sprintf("===%s is bleeding!===", monster.Name))
}
if monster.HP < 5 && monster.HP > 0 {
str.print(sprintf("===%s is dying!===", monster.Name))
}
if monster.HP <= 0 {
str.print(sprintf("===%s is dead!===", monster.Name))
}
}
func main () {
var player Player
player.Name = "Mario"
player.HP = 10
player.Mana = 10
player.Lives = 3
var monster Monster
monster.Name = "Domo-kun"
monster.HP = 7
monster.Mana = 4
player.draw()
monster.draw()
for true {
if player.HP < 1 || monster.HP < 1 {
return
}
printf("Command? (M)agic; (P)hysical; (E)xit\t")
var cmd str
cmd = read()
if cmd == "E" || cmd == "e" {
return
}
player.draw()
monster.draw()
player.attack(cmd, &monster)
monster.attack(cmd, &player)
battleStatus(player, monster)
}
}
Lastly, we can see how we use a for loop to create something similar to a REPL for the program that we have been building in the last few sections.
The last control flow mechanism is go-to, which is achieved through
the goto
statement.
package main
func main () (out i32) {
beginning:
printf("What animal do you like the most: (C)at; (D)og; (P)igeon\n")
var cmd str
cmd = read()
if cmd == "C" || cmd == "c" {
goto cat
}
if cmd == "D" || cmd == "d" {
goto dog
}
if cmd == "P" || cmd == "p" {
goto pigeon
}
cat:
str.print("meow")
goto beginning
dog:
str.print("woof")
goto beginning
pigeon:
str.print("tweet")
goto beginning
}
The program above creates an infinite loop by using goto
s. The loop
will keep asking the user to input commands, and will jump to certain
expression depending on the command.
CX has a small set of functions that are not associated to a single
type signature. For example, instead of using i32.add
to add two
32-bit integers, you can use the generalized add
function. Furthermore, whenever you use arithmetic operators, such as
+
, -
or %
, these are translated to their corresponding
"type-inferenced" function, e.g. num = 5 + 5
is translated to num = i32.add(5, 5)
. These native functions still follow CX's philosophy of
having a strict typing system, as the types of the arguments sent to
these native functions must be the same.
Note that after listing a group of similar "type-inferenced" functions below, we list the compatible types for the corresponding functions.
Note: the preceding functions only work with arguments of type bool, byte, str, i32, i64, f32 or f64.
package main
func main () {
bool.print(eq(5, 5))
bool.print(5 == 5) // alternative
bool.print(uneq("hihi", "byebye"))
bool.print("hihi" != "byebye") // alternative
}
Note: the preceding function only works with arguments of type byte, bool, str, i32, i64, f32 or f64.
package main
func main () {
bool.print(lt(3B, 4B))
bool.print(3B < 4B) // alternative
bool.print(gt("hello", "hi!"))
bool.print("hello" > "hi!") // alternative
bool.print(lteq(5.3D, 5.3D))
bool.print(5.3D <= 5.3D) // alternative
bool.print(gteq(10L, 3L))
bool.print(10L >= 3L) // alternative
}
Note: the preceding functions only work with arguments of type i32 or i64.
package main
func main () {
i32.print(bitand(5, 1))
i32.print(5 & 1) // alternative
i64.print(bitor(3L, 2L))
i64.print(3L | 2L) // alternative
i32.print(bitxor(10, 2))
i32.print(10 ^ 2) // alternative
i64.print(bitclear(5L, 2L))
i64.print(5L &^ 2L) // alternative
i32.print(bitshl(2, 3))
i32.print(2 << 3) // alternative
i32.print(bitshr(16, 3))
i32.print(16 >> 3) // alternative
}
Note: the preceding functions only work with arguments of type byte, i32, i64, f32 or f64.
package main
func main () {
byte.print(add(5B, 10B))
byte.print(5B + 10B) // alternative
i32.print(sub(3, 7))
i32.print(3 - 7) // alternative
i64.print(mul(4L, 5L))
i64.print(4L * 5L) // alternative
f32.print(div(4.3, 2.1))
f32.print(4.3 / 2.1) // alternative
}
Note: the preceding function only works with arguments of type byte, i32 or i64.
package main
func main () {
byte.print(mod(5B, 3B))
byte.print(5B % 3B) // alternative
}
Note: the preceding function only works with arguments of type str, arrays or slices.
package main
func main () {
var string str
var array [5]i32
var slice []i32
string = "this should print 20"
array = [5]i32{1, 2, 3, 4, 5}
slice = []i32{10, 20, 30}
i32.print(len(string)) // prints 20
i32.print(len(array)) // prints 5
i32.print(len(slice)) // prints 3
}
Note: the preceding function requires a format str as its first
argument, followed by any number of arguments of type str, i32,
i64, f32 or f64. The format string recognizes the following
directives: %s
for strings, %d
for integers and %f
for floating
point numbers.
package main
func main () {
var name str
var age i32
var wrongPI f32
var error f64
name = "Richard"
age = 14
wrongPI = 3.16
error = 0.0000000000000001D
printf("Hello, %s. My name is %s. I see that you calculated the value of PI wrong (%f). I think this is not so bad, considering your young age of %d. When I was %d years old, I remember I miscalculated it, too (I got %f as a result, using a numerical method). If you are using a numerical method, please consider reaching an error lower than %f to get an acceptable result, and not a ridiculous value such as %f. \n\nBest regards!\n", name, "Edward", wrongPI, age, 25, 3.1417, error, 0.1)
}
Note: the preceding function requires a format str as its first
argument, followed by any number of arguments of type str, i32,
i64, f32 or f64. The format string recognizes the following
directives: %s
for strings, %d
for integers and %f
for floating
point numbers.
package main
func main () {
var reply str
var name str
var title str
name = "Edward"
title = "Richard 8 PI"
reply = sprintf("Thank you for contacting our technical support, %s. We see that you are having trouble with our video game titled '%s', targetted to kids under age %d. If you provide us with your parents e-mail address, we'll be glad to help you!", name, title, 14)
str.print(reply)
}
package main
func main () {
var slc1 []i32
slc1 = append(slc1, 1)
slc1 = append(slc1, 2)
var slc2 []i32
slc2 = append(slc1, 3)
slc2 = append(slc2, 4)
i32.print(len(slc1)) // prints 2
i32.print(len(slc2)) // prints 4
}
The following functions are used to handle input from the user and to print output to a terminal.
package main
func main () {
var password str
for true {
printf("What's the password, kid? ")
password = read()
if password == "123" {
str.print("Welcome back.")
return
} else {
str.print("Wrong, but you'll get another chance.")
}
}
}
package main
func main () {
byte.print(5B)
bool.print(true)
str.print("Hello!")
i32.print(5)
i64.print(5L)
f32.print(5.0)
f64.print(5.0D)
printf("For a better example, check section Type-inferenced Functions'")
}
All parse functions follow the same pattern: XXX.YYY
where XXX is
the receiving type and YYY is the target type. You can read these
functions as "parse XXX to YYY".
package main
func main () {
var b byte
b = 30B
str.print(str.concat("Hello, ", byte.str(b)))
i32.print(5 + byte.i32(b))
i64.print(10L + byte.i64(b))
f32.print(33.3 + byte.f32(b))
f64.print(50.111D + byte.f64(b))
}
package main
func main () {
var num i32
num = 43
str.print(str.concat("Hello, ", i32.str(num)))
byte.print(5B + i32.byte(num))
i64.print(10L + i32.i64(num))
f32.print(33.3 + i32.f32(num))
f64.print(50.111D + i32.f64(num))
}
package main
func main () {
var num i64
num = 43L
str.print(str.concat("Hello, ", i64.str(num)))
byte.print(5B + i64.byte(num))
i64.print(10L + i64.i64(num))
f32.print(33.3 + i64.f32(num))
f64.print(50.111D + i64.f64(num))
}
package main
func main () {
var num f32
num = 43.33
str.print(str.concat("Hello, ", f32.str(num)))
byte.print(5B + f32.byte(num))
i32.print(33 + f32.f32(num))
i64.print(10L + f32.i64(num))
f64.print(50.111D + f32.f64(num))
}
package main
func main () {
var num f64
num = 43.33D
str.print(str.concat("Hello, ", f64.str(num)))
byte.print(5B + f64.byte(num))
i32.print(33 + f64.f32(num))
i64.print(10L + f64.i64(num))
f32.print(50.111 + f64.f32(num))
}
package main
func main () {
var num str
num = "33"
byte.print(5B + str.byte(num))
i32.print(33 + str.f32(num))
i64.print(10L + str.i64(num))
f32.print(50.111 + str.f32(num))
f64.print(50.111D + str.f32(num))
}
The assert
function is used to test the value of an expression
against another value. This function is useful to test that a
package is working as intended.
package main
func foo() (res str) {
res = "Working well"
}
func main () {
var results []bool
results = append(results, assert(5 + 5, 10, "Something went wrong with 5 + 5"))
results = append(results, assert(foo(), "Working well", "Something went wrong with foo()"))
var successfulTests i32
for c := 0; c < len(results); c++ {
if results[c] {
successfulTests = successfulTests + 1
}
}
printf("%d tests were performed\n", len(results))
printf("%d were successful\n", successfulTests)
printf("%d failed\n", len(results) - successfulTests)
}
package main
func main () {
bool.print(bool.eq(true, true))
bool.print(bool.uneq(false, true))
bool.print(bool.not(false))
bool.print(bool.or(true, false))
bool.print(bool.and(true, true))
}
package main
func main () {
str.print(str.concat("Hello, ", "World!"))
}
The following functions are of general purpose and are restricted to work with data structures of type i32 where it makes sense.
package main
func main () {
i32.print(i32.add(5, 7))
i32.print(i32.sub(6, 3))
i32.print(i32.mul(4, 8))
i32.print(i32.div(15, 3))
i32.print(i32.mod(5, 3))
i32.print(i32.abs(-13))
}
package main
func main () {
i32.print(i32.log(13))
i32.print(i32.log2(3))
i32.print(i32.log10(12))
i32.print(i32.pow(4, 4))
i32.print(i32.sqrt(2))
}
package main
func main () {
bool.print(i32.gt(5, 3))
bool.print(i32.gteq(3, 8))
bool.print(i32.lt(4, 3))
bool.print(i32.lteq(8, 6))
bool.print(i32.eq(-9, -9))
bool.print(i32.uneq(3, 3))
}
package main
func main () {
i32.print(i32.bitand(2, 5))
i32.print(i32.bitor(8, 3))
i32.print(i32.bitxor(3, 9))
i32.print(i32.bitclear(4, 4))
i32.print(i32.bitshl(5, 9))
i32.print(i32.bitshr(1, 6))
}
package main
func main () {
i32.print(i32.max(2, 5))
i32.print(i32.min(10, 3))
}
package main
func main () {
i32.print(i32.rand(0, 100))
}
The following functions are of general purpose and are restricted to work with data structures of type i64 where it makes sense.
package main
func main () {
i64.print(i64.add(5L, 7L))
i64.print(i64.sub(6L, 3L))
i64.print(i64.mul(4L, 8L))
i64.print(i64.div(15L, 3L))
i64.print(i64.mod(5L, 3L))
i64.print(i64.abs(-13L))
}
package main
func main () {
i64.print(i64.log(13L))
i64.print(i64.log2(3L))
i64.print(i64.log10(12L))
i64.print(i64.pow(4L, 4L))
i64.print(i64.sqrt(2L))
}
package main
func main () {
bool.print(i64.gt(5L, 3L))
bool.print(i64.gteq(3L, 8L))
bool.print(i64.lt(4L, 3L))
bool.print(i64.lteq(8L, 6L))
bool.print(i64.eq(-9L, -9L))
bool.print(i64.uneq(3L, 3L))
}
package main
func main () {
i64.print(i64.bitand(2L, 5L))
i64.print(i64.bitor(8L, 3L))
i64.print(i64.bitxor(3L, 9L))
i64.print(i64.bitclear(4L, 4L))
i64.print(i64.bitshl(5L, 9L))
i64.print(i64.bitshr(1L, 6L))
}
package main
func main () {
i64.print(i64.max(2L, 5L))
i64.print(i64.min(10L, 3L))
}
package main
func main () {
i64.print(i64.rand(0L, 100L))
}
The following functions are of general purpose and are restricted to work with data structures of type f32 where it makes sense.
package main
func main () {
f32.print(f32.add(5.3, 10.5))
f32.print(f32.sub(3.2, 6.7))
f32.print(f32.mul(-7.9, -7.1))
f32.print(f32.div(10.3, 2.4))
f32.print(f32.abs(-3.14159))
}
package main
func main () {
f32.print(f32.log(2.3))
f32.print(f32.log2(3.4))
f32.print(f32.log10(3.0))
f32.print(f32.pow(-5.3, 2.0))
f32.print(f32.sqrt(4.0))
}
package main
func main () {
f32.print(f32.sin(1.0))
f32.print(f32.cos(2.0))
}
package main
func main () {
bool.print(f32.gt(5.3, 3.1))
bool.print(f32.gteq(3.7, 1.9))
bool.print(f32.lt(2.4, 5.5))
bool.print(f32.lteq(8.4, 3.2))
bool.print(f32.eq(10.3, 10.3))
bool.print(f32.uneq(8.9, 3.3))
}
package main
func main () {
f32.print(f32.max(3.3, 4.2))
f32.print(f32.min(5.8, 9.9))
}
The following functions are of general purpose and are restricted to work with data structures of type f64 where it makes sense.
package main
func main () {
f64.print(f64.add(5.3D, 10.5D))
f64.print(f64.sub(3.2D, 6.7D))
f64.print(f64.mul(-7.9D, -7.1D))
f64.print(f64.div(10.3D, 2.4D))
f64.print(f64.abs(-3.14159D))
}
package main
func main () {
f64.print(f64.log(2.3D))
f64.print(f64.log2(3.4D))
f64.print(f64.log10(3.0D))
f64.print(f64.pow(-5.3D, 2.0D))
f64.print(f64.sqrt(4.0D))
}
package main
func main () {
f64.print(f64.sin(1.0D))
f64.print(f64.cos(2.0D))
}
package main
func main () {
bool.print(f64.gt(5.3D, 3.1D))
bool.print(f64.gteq(3.7D, 1.9D))
bool.print(f64.lt(2.4D, 5.5D))
bool.print(f64.lteq(8.4D, 3.2D))
bool.print(f64.eq(10.3D, 10.3D))
bool.print(f64.uneq(8.9D, 3.3D))
}
package main
func main () {
f64.print(f64.max(3.3D, 4.2D))
f64.print(f64.min(5.8D, 9.9D))
}
The functions in the time
package deal with real-time in your
programs. They are used to measure and stop time. Note that in order
to use these functions you need to import the time
package.
package main
import "time"
func main () {
var start i64
var end i64
start = time.UnixMilli()
time.Sleep(1000)
end = time.UnixMilli()
printf("elapsed time in milliseconds: \t%d\n", end - start)
start = time.UnixNano()
time.Sleep(1000)
end = time.UnixNano()
printf("elapsed time in nanoseconds: \t%d\n", end - start)
}
The os
package provides functions that serve as an interface to CX's
underlaying operating system.
package main
import "os"
func main () {
var wd str
wd = os.GetWorkingDirectory()
var fileName str
fileName = str.concat(wd, "testing.cx")
os.Open(fileName)
os.Close(fileName)
}
"OpenGL is the premier environment for developing portable, interactive 2D and 3D graphics applications. Since its introduction in 1992, OpenGL has become the industry's most widely used and supported 2D and 3D graphics application programming interface (API), bringing thousands of applications to a wide variety of computer platforms. OpenGL fosters innovation and speeds application development by incorporating a broad set of rendering, texture mapping, special effects, and other powerful visualization functions. Developers can leverage the power of OpenGL across all popular desktop and workstation platforms, ensuring wide application deployment." This description was extracted from OpenGL's website (https://www.opengl.org/).
"GLFW is an Open Source, multi-platform library for OpenGL, OpenGL ES and Vulkan development on the desktop. It provides a simple API for creating windows, contexts and surfaces, receiving input and events." This description was extracted from GLFW's website (https://www.glfw.org/).
"The gltext package offers a simple set of text rendering utilities for OpenGL programs. It deals with TrueType and Bitmap (raster) fonts. Text can be rendered in various directions (Left-to-right, right-to-left and top-to-bottom). This allows for correct display of text for various languages." This description was extracted from gltext's website (https://github.com/go-gl/gltext).
The gltext
functions can be used to display character strings on
windows. Different fonts can be used by loading font files using gltext.LoadTrueType
.