Making simulations is annoying in most programming languages. You have to set up a bunch of boilerplate, and make it fast too, simply so that you can press a button and have a value increment according to a value. Simulgine aims to make this easier by removing the need for all of that.
Instead of writing out procedural code that details step by step for the computer how to get from state A of the simulation to state B of the simulation, for each value, you write out what you need to do for the next tick.
Simulgine can be easily compiled with cargo:
cargo build --releaseAfterwards, the binary is a REPL run with one argument for a directory of .sml files.
Each .sml file is a collection of classes, which contain fields. These fields all come attached with code, the field's body, which computes the next value of the field for the next tick.
Every Simulgine program must have a ROOT class that is the parent of all other classes.
A basic 'Hello, World!' would be this:
test_projects/hellosim
class ROOT {
string hello "Hello, world!";
}
In order to run this, save it in a file (let's say hello.sml) and put it in a
directory (let's say hellosim). You can then run it as (in hellosim's parent
directory):
/path/to/Simulgine hellosim
This will open the REPL:
Simulgine REPL Terminal
>>
At anywhere in the code, the root can be accessed with the keyword root.
It is case-insensitive:
Simulgine REPL Terminal
>> rOoT
[ROOT] ROOT {
hello: string = ""
}
The square brackets denote the type of the printed value, whilst the ':' is the type of the field.
Currently, the hello field is an empty string. This is because that is the
default value for a string - you can create a default value of any type by
executing instanceof [type]. (You can go in reverse by going typeof [value].)
However, since we have given the field a body, the body will be executed on
next tick. Tick the system by executing !tick in the REPL.
Simulgine REPL Terminal
>> rOoT
[ROOT] ROOT {
hello: string = ""
}
>> !tick
>> rOoT
[ROOT] ROOT {
hello: string = "Hello, world!"
}
This can not be a true hello world, becuase Simulgine does not print anything by itself. It merely runs the "simulation".
You can quit the REPL with !quit.
Fields can reference their previous value with the keyword this. This program
will simply count the number of ticks passed:
test_projects/simple_counter
class ROOT {
u64 counter this + 1;
}
Fields can reference another field in their class with the keyword parent.
test_projects/parent_and_counter
class ROOT {
u64 three 3;
u64 counter this + parent.three;
}
Fields have three access modifiers:
public: Can be accessed from any other class.protected: Can be accessed from the same class. Default.private: Cannot be accessed from any other field.
And also three volatility modifiers:
const: This value never changes except by external code.level: For fields that are not instances of a class, same asconst. For fields that are, the object is ticked when the parent is.volatile: The value is recomputed from scratch every tick. Default.
Access and volatility modifiers come before the type, and in that order.
You can use curly braces in fields. This may be for purely aesthetics purposes, or to chain multiple statements together. The last statement will be returned, and all statements must end with a semicolon.
test_projects/simple_braces
class ROOT {
string hello {
"this does nothing.";
"Hello, world!";
};
}
You can also use them anywhere else:
test_projects/lots_braces
class ROOT {
string hello {
{
"this does nothing.";
"still doing nothing.";
};
{
{
{
{
"indentation generator";
};
};
};
};
"Hello, world!";
};
}
Simulgine, by default, computes all the values at once. This is a flagship feature, and allows for massive parallelization, as well as eliminating all race conditions. This means that every field can only see stale values from other fields.
This is acceptable in most cases, but sometimes syncronization is required. In this case, a turbofish can be added after a field's name in order to order it. By default, fields have a stage of 1. A field will compute only after all fields in the class with a lower stage than it have finished being computed, and will see the newer values.
This will take two !ticks for follower to become "Hello, world!":
test_projects/nostaging
class ROOT {
string hello "Hello, world!";
string follower parent.hello;
}
This will only take one !tick:
test_projects/yesstaging
class ROOT {
string hello "Hello, world!";
string follower::<2> parent.hello;
}
Be careful with this feature, especially on classes higher up in the hirearchy. Staging directly interferes with Simulgine's regular functioning of parallelization.
Simulgine allows for if statements. If statements are worded like so:
if [condition] [onTrue] {optional: else [onFalse]}
onTrue and onFalse are both expressions and not statements. They
do not end with a semicolon unless surrounded by curly braces. The if statement
itself is also an expression: It returns None without an else clause (except in
the REPL) and returns whichever subexpression was computed when it does have an
else clause.
Try this:
test_projects/ifwithbrace
class ROOT {
u64 counter this + 1;
string hello::<2> {
if parent.counter > 3
"counter greater than three"
else
"counter less than or equal to three";
};
}
The curly braces are purely aesthetics here; this works too:
test_projects/ifwobrace
class ROOT {
u64 counter this + 1;
string hello::<2>
if parent.counter > 3
"counter greater than three"
else
"counter less than or equal to three";
}
Some complex calculations are more convenient with intermediate values. For these, Simulgine offers variables, which are declared with the following:
let [name]: [type] = [initial value];
The variable can then be accessed with its name:
{
let randomInteger: u64 = 3;
randomInteger; // returns 3
}
Variables cannot be used outside of curly braces, and they belong to their braces. They can also be aliased.
class ROOT {
u64 counter let previous = this; // No no no no!!!
}
test_projects/lotslets
class ROOT {
u64 someRandomValue {
let useless: u32 = 3;
let other: u32 = {
let useless: u16 = 723
useless; // 723
};
other = useless + other; // 726
let useless: u32 = 8;
other + useless; // 734
};
}
You can access an outer braces variable from a more inner one, as well.
test_projects/lotserletser
class ROOT {
u64 someRandomValue {
let useless: u32 = 3;
let moreUseless: u16 = 723;
let other: u32 = {
let useless: u16 = moreUseless;
useless;
};
other = useless + other;
let useless: u32 = 8;
other + useless;
};
}
Classes may contain other classes in their fields. Volatility modifier of level is most appropriate in most situations. Please note that staging only applies within classes, not between them.
test_projects/nesting
class Finances {
double income 5.0;
double expenses 2.0;
public double profit::<2> parent.income - parent.expenses;
}
class Company {
double stockPrice::<2> this + parent.finances.profit / 10.0;
level Finances finances;
}
class ROOT {
level Company incorporationsIncorporated;
}