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.
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.
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.
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.
| Primitive | Description |
|---|---|
nil | Denotes the lack of a value |
f64 | 64 bit floating point (decimal) number |
i64 | 64 bit integer (whole) number |
bool | Boolean, either true or false |
dyn | Pointer to a dynamic value |
| Dynamic | Description |
|---|---|
str | String of characters, aka text |
err | Error value containing a reason string |
obj | Object made of map and vector components |
fun | Callable 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.
io.println(global.defined); # nil, undefined
global.defined = 4;
io.println(global.defined); # 4f64
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.
2f / 1.5f * 88.0 # 117.333fi64
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.
2 / 3 # 0
2 * 2 / 4 # 2bool
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.
var c = true;
c = !c;
c = -c; # same as !cstr
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.
val str = 'hi';
io.println(str[0]); # h
val o = {};
o[str] = 2;
io.println(o.hi); # 2err
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.
val fun = [](arg) {
if arg.type() == 'i64'
err('We hate integers!')
arg
};
fun(4).unwrap(); # Runtime panic: We hate integers!
return fun(4f).unwrap(); # 4fobj
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.
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); # 3fun
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.
[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