Introduction
Welcome to the Solstice docs! For now, the documentation is in the format of a specification, which instructs you on how Solstice should behave. In future, a tutorial (similar to the Rust book) will be written which shows you each part of the language, instead of declaring it like the spec.
Docs generated by mdBook.
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.
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