CX basics syntax - SkycoinWikis/CX GitHub Wiki

CX HOME » CX BASICS » SYNTAX

Syntax

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.

Table of Contents

Comments

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!")
*/

Declarations

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.

Allowed Names

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.

Strict Type System

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).

Primitive Types

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

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.

Local 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

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.

Slices

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.

Literals

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

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.

Custom Types

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.

Methods

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.

Packages

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 imports both the foo and bar packages, to later call each of these functions.

Statements

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.

If and if/else

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.

Else if

Instead of simply adding one alternative path, you can string together a series of else if blocks, which check for as many different conditions as you like. Giving you similar functionality as Go's switch/select blocks (containing various conditions/cases).

package main

func main () {
   var i i32
   i = 0
   
   if i == 0 {
     str.print("i is 0")
   } else if i == 1 {
     str.print("i is 1")
   } else if i == 2 {
     str.print("i is 2")
   } else {
     str.print("i is NOT 0, 1 or 2")
   }
}

For loop

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

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

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.

Assignments and Initializations

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.

Affordances

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)
}