An Introduction to solus
solus is a simple, experimental embedded language that aims to replace Lua in my projects. I felt Lua was missing many features I loved from other languages so I set out to learn how to make a register based bytecode interpreter, and then build the language and syntax around it. This language had many features planned from the start, but also many features that have been or will be added just because I thought it would be cool.
The Name
solus, always styled as lowercase, is a name that has two meanings. The first part is the .sol extension, sol being latin for sun and the sun being the flipside of the moon, which of course the name Lua refers to. And of course, the full name “solus” is another latin word, meaning alone or by oneself. solus is entirely developed by me and all of its choices reflect my own opinions on what is and isn’t good in other languages, but also what I feel is feasible for me as a single individual.
Why Alone?
If you do want to help with solus, you can do so by either sifting through the code and finding bugs, or by writing code yourself in the language and finding edge cases I may have missed. I hope that by the 1.0 release of this language it’ll be something I can seriously be proud to show off, so I’m not really open to working with other people on it directly. Plus, the codebase is written in plain C and not exactly beginner friendly C either, it would take a lot of effort to teach other people to work around my code in ways that I would be satisfied with. And plus, the name would be really fucking dumb if I did that.
The STD
The std, or standard library, are the functions and globals that are built in to the solus language, but aren’t always necessarily available in all solus environments. The standard library is optionally loaded by the C api, and is always loaded in the CLI tool. All values in the global namespace CAN be overridden for now, but it is likely that readonly objects will be added in the future.
sol.version: str
The version string of the solus VM you’re running currently
sol.git: str
The github repository link for solus :)
Builtin Functions
These are the functions that are defined in the global namespace, they have no module prefix and are the core functions of the language, its flow, and error handling.
import(path: str)
Loads a solus file and then executes it. If the file is not found at first and the .sol file extension is not provided, it will attempt to find path + '.sol'.
Parameters
path: The path to the solus file. The .sol extension will be attempted automatically if excluded.
Returns
any: If execution succeedserr: If the path cannot be found or the execution fails
Example
#![allow(unused)]
fn main() {
let module = unwrap(import("module"));
if type(module) == "err":
io.println("Failed to load module.sol");
else:
io.println(module.version);
}
require(path: str)
Loads a solus file and then executes it, behaving identically to import besides the fact that it panics instead of returning an error.
Parameters
path: The path to the solus file. The .sol extension will be attempted automatically if excluded.
Returns
any: If execution succeedspanic: If the path cannot be found or the execution fails
Example
#![allow(unused)]
fn main() {
let module = require("notfound"); // This will stop execution!
let mod2 = catch([](){ require("notfound") }); // Same as import("notfound")
}
eval(src: str)
Compiles source code from a string into a fun, note that this is NOT sandboxed and is NOT safe to accept use input with. Treat eval very carefully.
Parameters
src: Solus source code stored in a string
Returns
any: On successful evaluationerr: On compile error or runtime panic
Example
#![allow(unused)]
fn main() {
let cat = "{ meows = 2 paws = 4 }";
let instance = eval(cat);
io.println(instance.meows); // 2
}
panic(err: str)
Creates a runtime panic, solus’ equivalent to an exception pretty much. This will exit the program if it is not DIRECTLY caught.
Parameters
err: The reason for the runtime panic
Returns
panic: A fatal error type that exits the program if not caught
Example
#![allow(unused)]
fn main() {
if fatal_err_condition:
panic("ohhhh shit!");
}
catch(try: fun)
Catches any panics thrown within the provided try block’s scope of execution. Think of this as a try/catch that returns a result instead of passing it.
Parameters
try: A function that may panic or return an err
Returns
any: Whatever the provided tryfunreturnserr: The caught panic if one is thrown, converted to an err
Example
#![allow(unused)]
fn main() {
let mod = catch([](){ require("notfound") }); // Same as import("notfound")
}
attempt(try: fun, handler: fun(err))
Similar to catch except it uses a proper handler fun, mimicking try/catch behavior in other languages. If `
Parameters
try: A function that may panic or return an errhandler: A handler function that optionally accepts the caught panic/returned error as anerr
Returns
any: Whatever the provided tryfunreturns if it succeeds, or whatever the provided handlerfunreturns
Example
#![allow(unused)]
fn main() {
let mod = attempt(
[]() { return import("doesnt-exist.sol"); },
[](err) { io.println(err); return {}; }
);
}
unwrap(val: any)
Unwraps a variable that is ambiguously value or error, returning either the value or panicking on err
Parameters
val: Either a value or anerr
Returns
any:valis a valid valuepanic:valis of typeerr
Example
#![allow(unused)]
fn main() {
let mod = unwrap(import("")); // Panics on failure like require("")
}
unwrap_or(valid: any, invalid: any)
Returns valid if it is a valid value, or returns invalid if valid is err
Parameters
valid: A value to check the validity ofinvalid: The fallback case ifvalidis not
Returns
any: Eithervalidorinvalid
Example
#![allow(unused)]
fn main() {
let cfg = unwrap_or(do {
let f = io.fread("test.cfg");
if type(f) == "err": f
else: eval(f)
}, do {
unwrap(io.fwrite("test.cfg", obj.stringify(default, false)));
default
});
}
assert(con: bool)
Throws a panic if the provided condition is false
Parameters
con: Boolean condition
Example
#![allow(unused)]
fn main() {
assert(1 == 1);
assert(1 == 2); // Panic
}
type(val: any)
Returns the type of a value represented as a string
Parameters
val: Any value possible in the solus language
Returns
str: The typename of the value provided
Example
#![allow(unused)]
fn main() {
let module = unwrap(import("module"));
if type(module) == "err":
io.println("Failed to load module.sol");
else:
io.println(module.version);
}
()
str(val: any)
Converts a value of any type into a string representation. Note that this function does not stringify objects, but rather gives a string representation of the pointer.
Parameters
val: A value of any type.
Returns
str: A string representation of the value at its most basic.
Example
#![allow(unused)]
fn main() {
let x = 4; // i64
let y = {}; // obj
io.println("x: " + str(x) + ", y: " + str(y)); // "x: 4, y: 0x71d00c0d8"
}
err(str: str)
Constructs an err type from a str. You can return this to represent failure conditions.
Parameters
str: The reason of error or failure condition
Returns
err: An error or failure condition internally represented by a string
Example
#![allow(unused)]
fn main() {
let fun = [](bool) {
if bool: "success"
else: err("failure!")
};
}
i64(f64: f64)
Converts an f64 value into the i64 equivalent value. This will result in loss of floating point (decimal) precision.
Parameters
f64: An f64 value to be converted into i64
Returns
i64: The i64 equivalent to the f64 value provided
f64(i64: i64)
Converts an i64 value into the f64 equivalent value. Not all values i64 represents can be represented by f64 accurately.
Parameters
i64: An f64 value to be converted into i64
Returns
f64: The f64 equivalent to the i64 value provided
io Module
This module provides basic input/output capabilities to the solus VM so that you can actually see what your program is doing. This also includes some other system info.
io.print(val: any)
Prints the str representation of any value to the console
Parameters
val: Any valid value in the solus language
Example
#![allow(unused)]
fn main() {
io.print("Hello!");
}
io.println(val: any)
Prints the str representation of any value to the console, followed by a newline character and flush
Parameters
val: Any valid value in the solus language
Example
#![allow(unused)]
fn main() {
io.println("Hello, World!");
}
io.time()
Returns the time in seconds since the UNIX epoch
Returns
f64: Time in seconds since the UNIX epoch, represented as an f64
io.fread(path: str)
Attempts to read a file from the specified path
Parameters
path: The relative path to the file
Returns
str: The string contents of the file on successerr: The reason for read failure
Example
#![allow(unused)]
fn main() {
let f = io.fread("test.cfg");
eval(unwrap(f));
}
io.fwrite(path: str, content: str)
Writes the provided string to a file at the specified path
Parameters
path: The relative path to the file
Returns
err: The reason for write failure, if encountered
Example
#![allow(unused)]
fn main() {
unwrap(io.fwrite("test.cfg", obj.stringify(default, false)));
default
}
string Module
This module provides functions for manipulating str types. Note that strings in solus can be concatenated using the same + operator as any other value, which is why there is no string.cat(a, b)!
string.sub(str: str, start: i64, end: i64)
Returns a substring using the specified start and end. End must not be before start, and both indices are clamped
Parameters
str: The string to slicestart: The start index (starting at 0)end: The end index
Returns
str: The substring, which may be empty
string.len(str: str)
Returns the length of a string
Parameters
str: The string to get the length of
Returns
i64: The length of the string, in characters
obj Module
This module provides functions that allow you to interact with objects without using the built in operators and construction methods. This is useful for non-identifier friendly keys.
obj.new()
Constructs a new object with no members
Returns
obj: An empty object
obj.set(obj: obj, key: str, val: any)
Sets the object’s member at the specified key. Not all keys set this way can be accessed with the postfix obj.member operator
Parameters
obj: The object to call set onkey: The member name to store the value atval: The value to store at the specified member name
Example
#![allow(unused)]
fn main() {
let o = {};
obj.set(o, "wow", 1);
io.println(o.wow); // 1
obj.set(o, "Can't do this with . lol", 2);
}
obj.set(obj: obj, key: str, val: any)
Gets the object’s member at the specified key. Useful for non identifier friendly member keys
Parameters
obj: The object to call get onkey: The member name to attempt to get
Returns
any: If the member existserr: If the member does not exist
Example
#![allow(unused)]
fn main() {
let x = obj.get(o, "Can with this :)");
}
math Module
This one is a brief module because these math functions are common in basically every programming language, so I’ll keep the explanations short and straight to the point.
math.mini(a: i64, b: i64)
Returns the minimum of two i64 values
Returns
i64: The lower value between parametersaandb
math.maxi(a: i64, b: i64)
Returns the maximum of two i64 values
Returns
i64: The higher value between parametersaandb
math.minf(a: f64, b: f64)
Returns the minimum of two f64 values
Returns
f64: The lower value between parametersaandb
math.maxf(a: f64, b: f64)
Returns the minimum of two f64 values
Returns
f64: The higher value between parametersaandb
math.randi(min_v: i64, max_v: i64)
Returns a random integer between a minimum and maximum value, inclusive
Returns
i64: A random integer between your provided minimum and maximum
math.randf(min_v: f64, max_v: f64)
Returns a random number between a minimum and maximum value, inclusive
Returns
f64: A random number between your provided minimum and maximum