Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 succeeds
  • err: 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 succeeds
  • panic: 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 evaluation
  • err: 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 try fun returns
  • err: 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 err
  • handler: A handler function that optionally accepts the caught panic/returned error as an err

Returns

  • any: Whatever the provided try fun returns if it succeeds, or whatever the provided handler fun returns

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 an err

Returns

  • any: val is a valid value
  • panic: val is of type err

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 of
  • invalid: The fallback case if valid is not

Returns

  • any: Either valid or invalid

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 success
  • err: 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 slice
  • start: 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 on
  • key: The member name to store the value at
  • val: 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 on
  • key: The member name to attempt to get

Returns

  • any: If the member exists
  • err: 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 parameters a and b

math.maxi(a: i64, b: i64)

Returns the maximum of two i64 values

Returns

  • i64: The higher value between parameters a and b

math.minf(a: f64, b: f64)

Returns the minimum of two f64 values

Returns

  • f64: The lower value between parameters a and b

math.maxf(a: f64, b: f64)

Returns the minimum of two f64 values

Returns

  • f64: The higher value between parameters a and b

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