Skip to content

The Basics

In this chapter you'll learn about the basic building blocks of code in solus and how to use them. This will prepare you for the next chapter where you'll get to see the statements that arrange these expressions into functional programs. If you've written an interpreted language before like Lua or Python many if not all of these concepts will be very familiar to you.

Values

All variables and literals in solus refer a value. A value is some data plus information about its type, and in the case of dynamic types metadata as well. All values are in fact the same size, so using a bool versus an i64 yields the same memory usage. Dynamic values include types stored on the heap, and they can define special behavior. Assigning a dynamic value to another variable will 'point' to the same instance, unless copied.

solus
var x = 4;
val y = 2.2f;

Values can be assigned to local variables using the var (mutable) and val (mutable) keywords. Local variables are the fastest to access and should always be used over global variables when possible, the reasons for which I will explain in the next section. Local variables also must be explicitly captured, another concept that will be explained later. All function arguments are also stored as local variables in the first register slots.

solus
my_val = 'hello!';
io.println(global.my_val);

Global variables can be declared by omitting the var/val keyword if that name is not in use in the current scope. A global variable that has not yet been set will evaluate to nil. In case of name shadowing, the global keyword refers to the global namespace object for direct access. You should avoid polluting the global namespace, as object accesses become slower the more members they contain, and local variables are stored directly in the VM registers.

solus
var o = {};
o.x = 4;
o[0] = 'meow :3';

The last place that you will find values stored is inside objects, just like the global namespace object. Objects act as both valmaps and valvecs, names that refer to the individual map and vector components of the object. Accessing members from objects is typically extremely fast, especially with shorter member names (<40) due to string optimization.

Types

As previously mentioned, every value has a type. There are two different sets of types in solus: ptypes (primitive types) and dtypes (dynamic types), which I will refer to as simply 'primitives' and 'dyns' going forward for simplicity. Every value is a primitive, but not every value is a dyn.

PrimitiveDescription
nilDenotes the lack of a value
f6464 bit floating point (decimal) number
i6464 bit integer (whole) number
boolBoolean, either true or false
dynPointer to a dynamic value
DynamicDescription
strString of characters, aka text
errError value containing a reason string
objObject made of map and vector components
funCallable unit of solus or C code

nil

Nil, often referred to as null in other languages, is the lack of a value. Nil itself has no value, no comparison will be made outside of the type when checking for nil internally. When accessing a missing member or array index from an object you will be met with nil.

solus
io.println(global.defined);     # nil, undefined
global.defined = 4;
io.println(global.defined);     # 4

f64

The f64 type is a 64 bit IEEE 754 floating point number. This is a relatively accurate way to store floating point decimal numbers, meaning the decimal point can have any number of digits before or after it. This type is often simply called "number" in other scripting languages. The f suffix is optional, and can be used to distinguish f64 from i64 with whole numbers.

solus
2f / 1.5f * 88.0    # 117.333f

i64

The i64 type is a 64 bit integer, a type that can represent whole numbers in the range -2^63 to 2^63 - 1. A good rule to live by is to always default to integers and only use floats when necessary, as integers offer superior precision, comparisons, and often speed. Though in solus the speed of operations between integers and floats is negligible.

solus
2 / 3       # 0
2 * 2 / 4   # 2

bool

Boolean refers to a value that can only be true or false. It is important not to think of the bool type in solus as a number as it may be in other languages, where any non-zero value is true, as the bool in solus is strictly true and false. Negating a bool will result in the opposite value from the original as a result of this.

solus
var c = true;
c = !c;
c = -c;     # same as !c

str

Strings are the second most important types in any language behind numbers, they're the text that you use to look up members and output to the console. Strings are shorthandedly referred to as str in solus, outside of the name of the std module. Strings are immutable in solus as they are in many scripting languages due to an optimization technique known as String interning.

solus
val str = 'hi';
io.println(str[0]); # h
val o = {};
o[str] = 2;
io.println(o.hi);   # 2

err

Error type that is part of the result system. You can call .unwrap() on any variable, and if it is an error it will be escalated to a runtime panic (aka crash/halt). These can be used for simple error handling, returning either a good value or an err, and then checking the type of or unwrapping the result after the fact.

solus
val fun = [](arg) {
    if arg.type() == 'i64'
        err('We hate integers!')
    arg
};

fun(4).unwrap(); # Runtime panic: We hate integers!
return fun(4f).unwrap(); # 4f

obj

Objects contain two main components, the valmap and the valvec. These two components make up the map and vector parts of it respectively, and allow you to index "members" by both string and integer. The global namespace object is an example of an object that is often indexec by strings, but you may also use them just like they were arrays. New objects can be created by concatenation.

solus
val main_o = { b = 4 };
val copy_o = {} + main_o;
val ref_o = main_o;

main_o.a = 2;  # New member
main_o -= 'b'; # Delete member
main_o += { c = 3 }; # Append members

io.println(main_o.a); # 2
io.println(copy_o.a); # nil
io.println(ref_o.a);  # 2
io.println(ref_o.c);  # 3

fun

Functions are the main units of solus code, whether already compiled (solc) or run from source, every file is in reality just one big function containing many smaller functions and returning a value. Functions are made up of a few components I'll go over here, but you'll have to wait until the next chapter to see how they're called and used commonly in code.

solus
 [self, local_var]      (arg1, arg2)            { arg1 + arg2 }
# ^- Captures           ^- Arguments            ^ Code / Return
# These are the         Args are passed at      This is the main block
# values you wish       the call site and       of the function and
# to take in from       act the same as         the code it executes.
# the outer scope.      local variables.        Braces are optional.

var x = 2;
var fun = [x](new_val) x = new_val; # One liner returns its expression
io.println(fun(4)); # 4
io.println(x);      # 4