Introduction
Welcome to the Solstice documentation! Here we have a tutorial (starting here) and a specification for the Solstice programming language.
If you’re new to Solstice, I’d recommend starting the tutorial, at the installation page. If you’ve already installed Solstice, start at the Basics page.
Docs generated by mdBook.
Installation
Solstice is fully supported and tested on Linux, however should work on most UNIX-like environments. Windows support is theoretically possible, however not all features are avaliable, so use WSL, MSys, or Cygwin instead.
Which method is for you?
Use a prebuilt binary if you’re in a rush, or you can’t get a compiler on your system.
Use the automated build script for a full install of Solstice and Ground, with maximum flexibility.
Do a manual build if you intend to contribute to the Solstice source code.
Prebuilt Binaries
Solstice binaries statically linked with Ground are avaliable from the Solstice releases page. For now, these are only for Linux x86_64, but more options may be avaliable in future.
Download the latest release, copy it into a folder in your path (such as /usr/local/bin) and enjoy!
Producing release binaries
There is a script on Chookspace which builds Solstice and Ground in a container. Use this script for building production releases, and determining whether bugs are setup-specific or affect all people.
Use this script to produce prebuilt binaries for the releases page.
Build dependencies
macOS
xcode-select --install
Then, download UTHash here, and copy the contents of the include folder to /usr/local/include:
sudo cp (/path/to/uthash-master)/include/* /usr/local/include
Ubuntu/Debian
sudo apt install gcc git make uthash-dev
Arch Linux
sudo pacman -S --needed gcc git make uthash
Automated Build Script
This method requires the following installed:
- uthash (in
/usr/includeor similar) - A gcc-compatible C compiler linked to
cc- GCC and Clang are both tested and will yield similar results.
- Git (to get source code)
- Make (to organise building the code)
Refer to Build Dependencies for instructions on how to install dependencies.
Running the installer
Run this command in your terminal:
bash -c "$(curl -fsSL https://sols.dev/install.sh)"
This will:
- Download a shell script from
https://sols.dev/install.sh - Run the contents of the shell script in the
bashcommand interpreter
The script does the following:
- Checks if Ground and Solstice are installed on your system.
- If either Ground or Solstice aren’t avaliable, it will download, build, and install the source code.
- If Ground or Solstice are installed, it will update them to the latest version.
Building Manually
Refer to Build Dependencies for instructions on how to install dependencies.
Build and install Ground:
git clone https://chookspace.com/ground/ground
cd ground
make
sudo make install
cd ..
After this, build and install Solstice:
git clone https://chookspace.com/solstice/solstice
cd solstice
make
sudo make install
cd ..
Command Usage
Solstice programming language
Usage: solstice <file> [-h] [--help] [-p] [--print] [-b <file>] [--bytecode <file>] [-c <file>] [--compile <file>]
Args:
<file>: Solstice source file
-h or --help: Prints this help message and exits
-p or --print: Prints textual version of Ground bytecode to console
-b <file> or --bytecode <file>: Generates Ground bytecode (.grbc) and saves it to the provided filename
-c <file> or --compile <file>: Compiles Ground to Linux x86_64 assembly, outputs a binary to the provided filename (experimental)
If no extra arguments are provided, the generated Ground bytecode will be executed.
Arguments:
<file>
The Solstice source file to run.
There should never be more than one file, as Solstice allows files to import each other, rather than compiling multiple files into one manually.
-h or --help
Shows the help message above.
-p or --print
Prints the textual version of the generated Ground bytecode to the console.
This is useful for debugging the compiler, and ensuring the correct output is produced for the GroundVM.
-b <file> or --bytecode <file>
Outputs Ground bytecode (in grbc format) and saves it in the file <file>.
Ground bytecode is useful for distributing software in a format where the target machine does not need Solstice installed, only the Ground VM.
Run the compiled bytecode with:
ground -b <file>
-c <file> or --compile <file>
Compiles the Ground bytecode to assembly using the Tram backend, outputting a binary named <file>.
Requires Ground to be built with Tram support, and Tram to be installed on your system.
This backend is experimental and does not support all features.
Basics
This part of the tutorial shows you the basics of using Solstice. Click next to see the “Hello, World!” program!
Hello, World!
Create a new file on your computer (preferably in a new folder) named main.sols. In the file, write the following:
puts "Hello, World!"
Save the file, then run in your terminal:
solstice main.sols
Solstice will run the file for you, which should print Hello, World! to the console.
Here’s what it does:
puts: Stands for “put something”. It’s Solstice’s built in “please print out this thing’s current state” operator."Hello, World!": A string. A string is a collection of characters. In Solstice, you denote a string by surrounding your text with double quotes (").
Next up: using variables!
Variables
Creating Variables
In Solstice, you can initialize a variable using the syntax:
name = value
where:
nameis the name of your variable. You can use all letters in variable names, as well as underscores and numbers.- The standard way to name your variables in Solstice is with
camelCase.
- The standard way to name your variables in Solstice is with
valueis some sort of value. This value can be:- an integer (number with no decimal place)
- a double (number with a decimal place)
- a string (collection of characters, as seen before)
- a character (a single letter)
- a boolean (either
trueorfalse)
We’ll look at types of values in the next part.
Recalling Variables
Recall a variable’s content by using it’s name:
puts name
where name is the name of your variable.
Comments
In Solstice, there are 3 ways to do comments:
// (Single-Line Comment)
Writing // tells Solstice to ignore the rest of the line.
Some examples:
puts 2 + 2 // should be 4
// This variable holds the status code from checkStatus()
statusCode = checkStatus("file.txt")
This comment should be used when writing your code’s logic in Solstice.
/* */ (Multiline Comment)
Any text in between /* and */ will be ignored. These comments should be used to document functions and structs (we’ll get to those later.)
Example:
/*
Adds two numbers together.
*/
def add(int a, int b) int {
return a + b
}
# (Shebang Comment)
Use this comment only for using Solstice as a script interpreter in a shebang. Effectively the same as //, but we recommend using // over #.
Values and Types
In Solstice, everything is a value, and every value has a type. Values can be moved around and passed to different functions, however to pass them around their type must be confirmed.
int
An int is a signed (meaning either positive or negative) 8-byte integer (meaning no decimal place).
Using this size, Solstice can accurately process numbers between -9,223,372,036,854,775,807 and 9,223,372,036,854,775,807.
Integers are represented as a collection of digits not seperated by anything, such as:
123
7483
423843269
double
A double is a signed 8-byte floating-point (meaning with a decimal place) number.
Using this size, Solstice can accurately process numbers between 15 and 17 digits long.
Doubles are represented as a collection of digits, with a dot (.) seperating the ones and tenths positions, such as:
3.14
432.543
5907432.432
string
A string is a collection of characters which end with a null byte (handled by Solstice).
Solstice can accurately process strings of any size, provided your computer has enough memory.
Strings are represented as a collection of characters surrounded by double quotes (""), such as:
"Hello, World!"
"Solstice is cool"
"Rust kinda mid"
char
A char is a signed 1-byte integer, represented as an ASCII character, however can also be used to represent a single byte in a stream.
Solstice can accurately process any ASCII character.
Characters are represented as a single character surrounded by single quotes (''), such as:
'a'
'b'
'c'
bool
A bool is a variable which can either be true or false.
true
false
Other Types
Solstice has three other types: fun, template, and object, but we’ll cover these in the Functions and Structures pages.
Functions
Structures
Naming Conventions
Comment Conventions
Code Layout
Reserved Words
This is a list of all reserved words in Solstice, seperated by new lines. These words may not be used as identifiers.
After //, notes may be provided which detail exceptions to the reserved word, however, the word should still be treated as reserved regardless.
puts
if
while
def
lambda
return
use
struct
enum
constructor
destructor
duplicator
private
protected
ground // Only when targeting GroundVM
new
as
sizeof
pragma
Comments
Comments in Solstice can be done in 3 ways:
// (Single-Line Comment)
Use // to tell Solstice to ignore text until the end of the line.
This kind of comment should be used inside code to explain how parts of code work.
/* */ (Multiline Comment)
Use /* and */ to tell Solstice to ignore text in between the two markers.
This kind of comment should be used for explainations of how functions and structs work, placed above the definition.
# (Shebang Comment)
Use # to tell Solstice to ignore text until the end of the line.
This kind of comment should be used in a shebang (writing #!/usr/bin/env solstice at the top of your entry point source file), not in the middle.
Example
#!/usr/bin/env solstice
/*
This function does something.
Input:
funnyNumber: A funny number
Returns: Another funny number
*/
def doSomething(int funnyNumber) int {
// Tell the user the number is funny
puts "The number" funnyNumber "is very funny"
// Now give them another funny number
return 532
}
Expressions
An expression in Solstice is a combination of operators and values (whether literal or stored in a variable).;
Expression Types
“Precedence” is a number which dictates the order of execution. The higher the precedence, the sooner an expression will be evaluated.
Binary Mathematical
+: Adds two numbers on either side, or concatenates two strings- Accepts:
- Two integers either side
- Two doubles either side
- Two strings either side
- An integer and a double on either side (order not significant)
- Returns:
- The sum of both values as an integer if both values are integers
- The sum of both values as a double if one value is a double and the other is an int, or both values are doubles
- The concatenated string when both values are strings
- Precedence: 5
- Accepts:
-: Subtracts two numbers on either side- Accepts:
- Two integers either side
- Two doubles either side
- An integer and a double on either side (order not significant)
- Returns:
- The difference of both values as an integer if both values are integers
- The difference of both values as a double if one value is a double and the other is an int, or both values are doubles
- Precedence: 5
- Accepts:
*: Multiplies two numbers on either side- Accepts:
- Two integers either side
- Two doubles either side
- An integer and a double on either side (order not significant)
- Returns:
- The product of both values as an integer if both values are integers
- The product of both values as a double if one value is a double and the other is an int, or both values are doubles
- Precedence: 6
- Accepts:
/: Divides two numbers on either side- Accepts:
- Two integers either side
- Two doubles either side
- An integer and a double on either side (order not significant)
- Two doubles either side
- Two integers either side
- Returns:
- The quotient of both values as an integer if both values are integers
- The quotient of both values as a double if one value is a double and the other is an int, or both values are doubles
- Precedence: 6
- Accepts:
Binary Comparative
==: Checks two values to determine if they are equal- Accepts:
- Any two values either side which are of the same type
- An integer and a double on either side (order not significant)
- Returns:
trueif both provided values are exactly the same.falseotherwise.
- Precedence: 2
- Accepts:
!=: Checks two values to determine if they are not equal- Accepts:
- Any two values either side which are of the same type
- An integer and a double on either side (order not significant)
- Returns:
trueif both provided values are not exactly the same.falseotherwise.
- Precedence: 2
- Accepts:
>: Checks two numbers to determine which is greater- Accepts:
- Any two numbers (int or double) either side
- Returns:
trueif the number provided on the left is greater than the number provided on the right.falseotherwise.
- Precedence: 2
- Accepts:
<: Checks two numbers to determine which is lesser- Accepts:
- Any two numbers (int or double) either side
- Returns:
trueif the number provided on the left is lesser than the number provided on the right.falseotherwise.
- Precedence: 2
- Accepts:
>=: Checks two numbers to determine which is greater, or whether both are equal- Accepts:
- Any two numbers (int or double) either side
- Returns:
trueif the number provided on the left is greater than the number provided on the right, or if both numbers are equal.falseotherwise.
- Precedence: 2
- Accepts:
<=: Checks two numbers to determine which is lesser, or whether both are equal- Accepts:
- Any two numbers (int or double) either side
- Returns:
trueif the number provided on the left is lesser than the number provided on the right, or if both numbers are equal.falseotherwise.
- Precedence: 2
- Accepts:
Binary Misc
as: Converts between types.- Accepts:
- Any object on the left, and a type which has an applicable conversion defined by the object on the right
- Returns:
- The object turned into the type on the right, in the way defined by the as-method
- Accepts:
.: Accesses object fields- Accepts:
- An object on the left, and an identifier on the right which corresponds to a field or method in the object
- Returns:
- The object field or method the identifier corresponds to
- Accepts:
Brackets
(...): Evaluates an expression inside the brackets before outside expressions.- Accepts:
- Any expression inside the brackets (in place of
...)
- Any expression inside the brackets (in place of
- Returns:
- The result of the expression in the brackets
- Precedence: 7
- Accepts:
Function Call
(...): Calls the specified function.- Accepts:
- An identifier for the function before the brackets, then comma-seperated expressions corresponding to the function type signature (see Types -> Combined Types -> fun) inside the brackets (in place of
...)
- An identifier for the function before the brackets, then comma-seperated expressions corresponding to the function type signature (see Types -> Combined Types -> fun) inside the brackets (in place of
- Returns:
- The function’s return type as specified by the function type signature (see Types -> Combined Types -> fun)
- Accepts:
Unary Misc
new: Creates a new object from a template.- Accepts:
- A template after the
newkeyword. - A core type identifier
- A template after the
- Returns:
- An object based on the provided template, if provided with a template.
- The empty version of the core type, if provided with a core type, which is:
int:0double:0.0string:""char:'\0'bool:false
- Accepts:
sizeof: Gets the size of something.- Accepts:
- A string
- An object with an
int sizefield
- Returns:
- The string length if provided a string
- The
int sizefield of the object if provided an object
- Accepts:
Types
Solstice is statically typed, meaning all values must have a known type before any code can be executed.
Core Types
These core types are automatically avaliable in Solstice. The words to identify them are reserved and cannot be reassigned.
int
8-byte signed integer, equivalent to C int64_t or Ground -int.
Examples: 32, 121, -5
double
8-byte double prescision floating point number, equivalent to C double or Ground -double.
Examples: 3.14, -2.7
string
C-style null-terminated array of characters. Equivalent to C char* or Ground -string.
char
1-byte signed integer, usually used to store a character, however can also be used to store bytes. Equivalent to C char or Ground -char.
bool
1-byte unsigned integer, either 1 or 0. Represented by the reserved words true and false. Equivalent to C char (no stdbool.h) or bool (with stdbool.h) or Ground -bool.
Combined Types
These types utilise core types to create new combinations. They take type arguments.
fun
Represents a function. Equivalent to Ground -function, or a C function pointer.
Syntax:
fun(...) type
where:
typeis a type identifier, which will be the return type...is multiple type identifiers, seperated by commas, which are the argument types
Example:
fun(int, string) bool
This represents a function which takes an int and string as arguments, and returns a bool.
template
Represents an uninitialized object. Equivalent to Ground -struct.
Syntax:
template(...)
where ... is multiple pairs of:
- a type identifier for the field, and
- an identifier (representing the field name)
seperated by commas.
Example:
template(int x, string y)
This represents a template which, when initialized, has fields x (with integer) and y (with string).
object
Represents an initialized object.
Syntax:
object(...)
where ... is multiple pairs of:
- a type identifier for the field, and
- an identifier (representing the field name)
seperated by commas.
Example:
object(int x, string y)
This represents a object which has fields x (with integer) and y (with string).
Variables
Variables are defined and modified with the syntax:
name = value
where:
nameis an identifier, andvalueis a literal, expression, or identifier previously assigned to a variable.
All variables must have a known type and value at definition time. When updating a variable, the type may not change.
Scoping and Lifetimes
Subscoping
A “subscope” is a scope contained inside a function, created inside curly braces ({}).
Subscopes can view and modify variables avaliable in their parent scope.
Subscopes may also create their own variables, however these will be destroyed when the subscope ends.
Function Scoping
A function scope is a scope created inside a function.
Function scopes are created from a function’s attached closure, which is a snapshot of all variables at the time of function definition.
The function’s attached closure cannot be modified.
Function scopes also contain all arguments provided to a function.
When the function finishes running (with a return), all variables are destroyed.
Destructors
Objects (see here) can contain a destructor which specifies custom behaviour for destroying itself. This is useful for:
- Freeing heap-allocated memory
- Closing a connection
- Closing a file
The destructor will always be in the field destructor with type signature fun() int. It should be automatically called when the variable goes out of scope.
Duplicators
Objects (see here) can contain a duplicator which specifies custom behaviour for copying itself. This is useful for:
- Duplicating heap-allocated memory
The duplicator will always be in the field duplicator with type signature fun((self) old) (self). It should be automatically called when the variable is:
- Copied into a new variable
- Put inside a closure
- Passed to a function
Operators
Operators are bits of code intended to control how the program runs.
puts
Stands for “put something”. Prints debug info to the console, followed by a new line.
Formatting
-
int: The literal number, as formatted by C
printf("%" PRId64). -
double: The literal number, as formatted by C
printf("%f"). -
string: All characters in the string, as formatted by C
printf("%s"). -
char: The ASCII character corresponding to the number held, as formatted by C
printf("%c"). -
bool: If
trueor1, printtrue. Otherwise, printfalse. -
fun: Print
<function>. -
template: Print
<struct fields: { ... }>, where:...is a comma-seperated sequence ofkey: value, where:keyis the identifier of the field.valueis the properly formatted value in the field.
-
object: Print
<object fields: { ... }>, where:...is a comma-seperated sequence ofkey: value, where:keyis the identifier of the field.valueis the properly formatted value in the field.
if
Runs a block of code only if the provided condition is true.
Syntax:
if condition {
...
}
where:
conditionis an expression which evaluates to a boolean.- If this expression evaluates to
true, the code block will be run. - Brackets surrounding the condition are optional.
- If this expression evaluates to
...is the code to run if the condition is true.
Example:
if 2 + 2 == 4 {
puts "phew!"
}
while
Runs a block of code until the provided condition is false.
Syntax:
while condition {
...
}
where:
conditionis an expression which evaluates to a boolean.- If this expression evaluates to
false, the code block will not be run anymore. - Brackets surrounding the condition are optional.
- If this expression evaluates to
...is the code to run while the condition is true.
Example:
while true {
puts "To infinity and beyond!"
}
return
Returns a value from a function.
Syntax:
return value
where value is a value with the same type as the function’s return type.
ground
Inline Ground is potentially dangerous. Only use if you know what you’re doing!
Inserts inline Ground code into the generated program.
Syntax:
ground {
...
}
where ... is the Ground code to insert.
See here for details about Ground.
Note: Solstice cannot keep track of state inside ground blocks! Ground will allow you to do things without the overhead of the Solstice type checker.
To extract a value from a Ground block, do something like this:
x = 0
ground {
set &x 10
}
puts x
so the type checker can know what x is.
use
The use operator lets you import code from libraries, either from in the local folder or installed in the $SOLSTICE_LIBS directory, which defaults to /usr/lib/solstice.
When using use, Solstice will insert the selected file’s contents at that position.
Local use
Supply a string after use which is the path to the file to import, without the .sols extension.
use "parser/Node"
use "parser/parser"
Global use
Supply an identifier after use which is the name of the library to import, without the .sols extension.
use io
Function Definition
Named Functions
Define a named function with the following syntax:
def functionName(args) type {
...
}
where:
functionNameis the name of the function.- If
functionNameis already defined as a function or other value, it will only be overwritten if the type does not change.
- If
argsis multiple comma-seperated pairs of:- a type identifier for the parameter, and
- an identifier (representing the parameter name)
typeis the type of value which the function will return...is the code inside the function (the function body).- The function body will have access to previously set variables through a closure. The closure is a copy of all variables in their current state at function definition time.
- The function body will also have access to the parameters defined in
args.
Example:
def add(int a, int b) int {
return a + b
}
Anonymous/Lambda functions
Define a lambda function with the following syntax:
lambda(args) type {
...
}
where:
argsis multiple comma-seperated pairs of:- a type identifier for the parameter, and
- an identifier (representing the parameter name)
typeis the type of value which the function will return...is the code inside the function (the function body).- The function body will have access to previously set variables through a closure. The closure is a copy of all variables in their current state at function definition time.
- The function body will also have access to the parameters defined in
args.
Example:
add = lambda(int a, int b) int {
return a + b
}
Lambda functions are most useful when passed as arguments to other functions.
Struct Definition
Define a struct with the struct keyword.
Syntax:
struct StructName {
...
}
where:
StructNameis the name of your struct...is the body of your struct
Struct Body
Inside the struct body, you can:
- Define fields
- Define methods
- Define special methods (
constructor,destructor,duplicator,as)
Defining fields
A field in a struct is defined in the same way as a variable.
struct MyStruct {
x = 5
}
This creates a field named x with a default value of 5.
Defining methods
A method in a struct is defined in the same way as a named function.
struct MyStruct {
def myMethod() int {
return 0
}
}
Methods have access to the struct’s members through the self object, which is implicitly passed.
struct MyStruct {
x = 10
def myMethod() int {
return self.x * 2
}
}
Private and protected fields
A private field is a field which only methods inside the object can read and write.
A protected field is a field which only methods inside the object can write to, however anywhere else in the program the field can be read.
All other fields are public, meaning anywhere in the program can read or write these fields.
Methods and special methods are by default protected, however can be made private.
struct MyStruct {
private x = 10
protected y = "Hi"
private doSomething() int {
return 14
}
}
Defining special methods
In Solstice, objects have some special methods which provide ease of use while handling these objects.
Constructor
A constructor is used to initialize an object with user-provided fields.
Define a constructor like this:
struct MyStruct {
constructor(args) {
...
}
}
where:
argsis multiple comma-seperated pairs of:- a type identifier for the parameter, and
- an identifier (representing the parameter name)
...is the constructor body- The constructor body has access to the
selfobject, which is how it can modify the newly created object. - The
selfobject is automatically returned at the end of the constructor, however can be returned manually if required.
- The constructor body has access to the
Use a constructor like this:
MyStruct(args)
where:
- args is many comma-seperated expressions corresponding to the signature of the defined constructor
Destructor
A destructor is used to destroy an object once it goes out of scope.
Define a destructor like this:
struct MyStruct {
destructor {
...
}
}
where:
...is the destructor body- The destructor body has access to the
selfobject, which is how it can access and modify the object which is being destroyed. - There is no need to return a value from the destructor.
- The destructor body has access to the
Note how the destructor does not take any arguments.
Duplicator
A duplicator is used to copy an object when it is assigned to a new name, or passed to a function.
Define a duplicator like this:
struct MyStruct {
duplicator {
...
}
}
where:
...is the duplicator body- The destructor body has access to the
selfandoldobjects.- The
selfobject is the new duplication and will be automatically returned - The
oldobject is the old object and will not be copied out
- The
- The destructor body has access to the
Note how the duplicator does not take any arguments.
As methods
As methods allow easy conversion between different types.
Define an as method like this:
struct MyStruct {
as type {
...
}
}
where:
typeis the type which the object will be converted to...is the as method body- The
selfobject is avaliable inside the as method - You will need to initialize and return a new value with the specified type
- The
Use an as method like this:
myobj as type
where:
myobjis an object with an as method which provides a conversion totypetypeis the target type
“Vela” 0.x.x
Vela is the codename for the 0.x.x releases of Solstice. It is named after the Vela constellation.
Changes
Vela is the prerelease version of Solstice, and is the phase where core compiler features are added. Here are some features added in Vela to the compiler:
puts- Variables
- Values
- Static type system
if,whilecontrol flow- Functions
- Lambda/Anonymous Functions
- Named Functions
- Closures attached to functions
- Structs
- Fields
- Methods
privateandprotectedas-methods- Constructors, destructors, duplicators
- Generics (in progress)
pragmadirectives (in progress)- Libraries with
use
“Vela” 0.1.0
This is the very first release of Solstice! Not much to see here.
This release contains:
puts- Variables
- Values
- Static type system
if,whilecontrol flow- Functions
- Lambda/Anonymous Functions
- Named Functions
- Closures attached to functions
- Structs
- Fields
- Methods
privateandprotectedas-methods- Constructors, destructors, duplicators
- Generics (partially, there are many bugs)
pragmadirectives in the parser- Libraries with
use
“Fornax” 1.x.x
Fornax will be the codename for the 1.x.x releases of Solstice. It is named after the Fornax constellation.
Release Target
Fornax will release when:
- The compiler core is stable enough to host moderately sized projects
- The standard library is capable enough for many programming projects
Fornax is not currently released.