Core Functionality

Variables

To create mutable variables, we use the var keyword. To create immutable constants, we use the const keyword.

var x = 12
const y = 100

x = 50 // A-Okay
y = 40 // Error

Variables can also be declared without values (they're implicitly set as None):

var x // Okay
const y // Error - const declarations must have a value

Loops

There are two types of loops, for and while.

For loops are constructed like so:

Basic for loop
for (1..100) {
    // do something
}
For loop with index
for (1..100, index) {
    println(index)
}
Index and value
for (1..100, index, value) {
    println(index + value)
}

For loops can also take lists as the first parameter:

For loop with list
const someList = ["a", "b", "c"]

for (someList, index, value) {
    if (value == "b") {
        println("Found a 'b'")
    }
}

While loops are constructed like so:

While loop
var i = 0

while (i < 10) {
    println(i)
    i += 1
}

Both for loops and while loops support the keywords break and continue.

break exits out of the current loop, and continue skips the current iteration.

Branching

Vortex, like many other languages, supports branching via if/else statements:

Branching
var x = 12
var y = 15

if (x > y) {
    println("x > y")
} else if (x == y) {
    println("x = y")
} else {
    println("x < y")
}

Imports

We can perform imports in two separate ways: module imports and variable imports.

Module Imports

Module imports allow you to import an entire file (module) into the current scope. The imported module can be used as an object:

import math : "../modules/math"

const res = math.fib(10)

Variable Imports

Variable imports allow you to pick and choose what you want to import into the local scope:

import [PI, e, fib] : "../modules/math"

const res = PI * e + fib(20)

You can also import all variables from a module into the current scope by simply leaving the import list blank:

import [] : "../modules/math"

Note that both imports have the same path, which is just a path to the module file. Do not include the extension .vtx in your import paths, the interpreter will do this for you under the hood.

Vortex also has a default module path it looks in if:

  • No path is specified in the import

  • @modules is used inside the import path

On Mac/Linux, the default path is in usr/local/share/vortex/modules/<module_name> and on Windows it's C:/Program Files/Vortex/modules/<module_name>.

Built-in modules will always reside in the default modules directory.

import sdl // Vortex looks for "sdl/sdl.vtx" in the default dir
import sdl : "@modules/sdl" // @modules resolves to the default dir path

Variable imports from a built-in module (or from a module the user has placed in the default modules directory) can also target the module's name, without needing a path string:

import [SDLInit] : sdl // Vortex will look for "sdl/sdl.vtx" in the default dir

Data Types

There are multiple data types in Vortex that you can use in your programs:

Number

The Number data type is implemented as a double, meaning it covers both integers and floating point numbers.

String

Strings can be used to express text and support the majority of escape characters.

Strings also support interpolation:

const name = "John"
const age = 34
const message = f"Hi, my name is ${name} and I am ${age} years old."

println(message)

// Hi, my name is John and I am 34 years old.

Boolean

Booleans are values that can either be true or false.

Lists

Lists are vectors that can contain any data type.

You can access list elements with the bracket syntax:

var list = [1, 2, 3]

list[1] = 100

println(list) // [1, 100, 3]

If you attempt to access a list element using a negative index or out of range index, None is returned.

Setting a value using a negative index will prepend the value, similarly setting a value with an out of range index will append it:

list[-1] = 20
list[100] = 30

println(list) // [20, 1, 100, 3, 30]

However, using the built-in append and insert functions is preferred:

list.append(10)
list.insert(100, 0)

println(lists) // [100, 1, 2, 3, 10]

Lists can also be constructed using destructuring:

const list_a = [1, 2, 3, 4]
const list_b = [...list_a, 5, 6, 7]

println(list_b) // [1, 2, 3, 4, 5, 6, 7]

List destructuring (using the spread operator ...) can only be used within another list.

Objects

Objects are essentially maps that can hold named properties:

var person = {
    firstName: "John",
    lastName: "Smith",
    age: 34,
    "fullName": () => this.firstName + " " + this.lastName,
    getAge: () => this.age
}

const name = person.fullName()
const age = person.getAge()
println(name) // John Smith
println(age) // 34

Notice the use of this in the object. Objects can refer to themselves within their own properties, but only in the context of a function.

Object properties can be constructed using both strings and identifiers.

Properties can be accessed in two ways, through the bracket notation or through the dot notation:

println(person["fullName"]) // "John Smith"
println(person.age) // 34
println(person.height) // None

Notice how attempting to access a property that does not exist simply returns None.

Object construction can also be done with object destructuring:

const obj_a = {
    a: 0,
    b: 1,
    c: 2
}

const obj_b = {
    ...obj_a,
    c: 2.5,
    d: 3,
    e: 4
}

println(obj_b) // { a: 0, b: 1, c: 2.5, d: 3, e: 4 }

Object destructuring (using the spread operator ...) can only be used within another object.

None

None is a special data type that refers to a variable that has no value.

Pointer

Pointer types are used to store raw C pointers. They are predominately used when dealing with external C modules.

Vortex does not support raw memory access, and so these types are reserved mainly for passing around pointers to and from dynamic libraries.

Function

Functions are also core types in Vortex, and can be passed around like other variables.

Functions can act as standard subroutines or as coroutines: Read more about coroutines here.

There is no function keyword in Vortex. All functions are lambdas assigned to variables.

const sayHi = (name) => println(f"Hello, ${name}!")

sayHi("James") // Hello, James!

Function parameters can have defaults, however you cannot declare non-default parameters after the default ones:

const add = (a, b = 10) => a + b

add(1, 2) // 3
add(10) // 20

You can declare a variadic parameter that will capture a variable amount of values. This parameter has to be the last declared parameter:

const doStuff = (...args) => {
    println(args)
}

doStuff(1, "a", "b", 3) // [1, "a", "b", 3]

The variables passed in become a list that can then be manipulated.

Another useful thing about variadic parameters is that you can use them to pass an unknown amount of arguments to inner functions:

const outerFunc = (innerFunc, ...args) => {
    const start = clock()
    const res = innerFunc(...args)
    const end = clock() - start
    return [res, end * 1000]
}

const inner = (a, b, c) => {
    return a + b + c
}

outerFunc(inner, 5, 4, 5).println()

// [14, 0.009]

In the case above, outerFunc doesn't need to care about how many arguments innerFunc takes. It simply takes the arguments in args passes them down to the provided function.

If no return value is declared, the function will return None.

Functions As Constructors

Functions are also used as constructors.

Vortex does not support classes, however class functionality can be achieved by using functions instead:

type Color = (r, g, b, a = 255) => ({
    r: r,
    g: g,
    b: b,
    a: a
})

const color = Color(144, 28, 42)

println(info(color).typename) // Color

Constructor functions simply return objects. However, note that the constructor wasn't defined with var or const. We used the type keyword here instead.

There's nothing magical about this keyword. It simply ensures that the object we return from the function receives the same typename as the constructor's name.

Getting the object's typename (via the info() built-in), we can see that it is indeed Color.

We're not limited to returning an object right away. Remember that Constructors are just functions, so we could have done this instead:

type Color = (r, g, b, a = 255) => {
    const cap = (n) => {
        if (n < 0) {
            return 0
        }
        if (n > 255) {
            return 255
        }
        return n
    }
    
    return {
        r: cap(r),
        g: cap(g),
        b: cap(b)
    }
}

const color = Color(144, 28, 42)

Instead of returning an object directly, we declared a cap function and returned an object with capped values. And since we declared the constructor with the type keyword, the returned object will have the typename Color.

Uniform Function Call Syntax

Vortex supports UFCS, allowing you to chain functions together in a seamless way.

Below is a simple example of showing the difference between normal function calls and UFCS:

const join = (a, b) => a + b

// Normal call
join("Hello", " world!") // "Hello world!"

// UFCS
"Hello".join(" world!") // "Hello world!"

This doesn't look too useful yet, however if we wanted to call this function multiple times to construct a longer sentence, UFCS remains easier to read and write:

// Normal call
join(join(join(join("Hello", " world!"), " My name is"), " Jordan"), " and I am 32 years old.") // "Hello world! My name is Jordan and I am 32 years old"

// UFCS
"Hello".join(" world!").join(" My name is").join(" Jordan").join(" and I am 32 years old.") // "Hello world! My name is Jordan and I am 32 years old"

/* Let's make that even clearer */

"Hello"
.join(" world!")
.join(" My name is")
.join(" Jordan")
.join(" and I am 32 years old.")

As you can see, the UFCS version is much more readable. And not only that, but it's much easier to modify. We can swap the calls to join around, add new ones in between, or remove some without dealing with nested calls.

Every function in Vortex can be called in this manner.

Last updated