This repository contains the source code for the Societal Construction Tool (SCT) compiler, as part of a project on Aalborg University.
The language aims to ease the creation of Agent-Based Modelling and Simulation (ABMS), via seperation of concern by encapsulating logic of agents and states.
The compiler is written in C#, and transpiles to C#.
The repository is split into two distinct projects
Project | Description |
---|---|
SctBuildTasks | The parser generator for unix-like systems |
SocietalConstructionTool | The main project containing the compiler |
SocietalConstructionToolTests | The test suite, ensuring the correctness of the compiler |
Download the latest release.
- Here you will find releases for Linux, MacOS and Windows.
Installing the compiler in Linux can be achieved via
wget https://github.com/DATP4G6/sct/releases/download/v1.0.1/sct
chmod +x sct
./sct --help
- Install .NET 8
- The compiler uses
antlr4
as a parser generator - it can likely be installed via your package manager (apt
,brew
,pacman
choco
etc.)
On Unix-like systems, the parser files are automatically generated before building as part of the build process.
On Windows, the parser code must be generated manually:
cd SctBuildTasks
antlr4 -Dlanguage=CSharp Sct.g4 -o out -visitor -no-listener
cd ..
Note
The .NET compiler automatically rebuilds all required projects when running and testing unless told explicitly not to using the --no-build
flag.
Run the tests
dotnet test
To run the project (-c
outputs simulation trace to the console (stdout)):
dotnet run --project SocietalConstructionTool -- -c <path-to-sct-file-1> <...> <path-to-sct-file-n>
Alternatively, the -o <output-file>
flag can be used to redirect the output to a file for further analysis.
Or with Nix with flakes enabled:
nix run .# -- -c <path-to-sct-file-1> <...> <path-to-sct-file-n>
Benchmarking requires the following additional dependencies:
hyperfine
to perform benchmarking- Python 3.11+ to generate the benchmark files
To run the benchmarks, run the following commands:
cd benchmark
./runBenchmark.sh <size>
The size
argument given to the runBenchmark
script determines the number of benchmark files to generate and run.
Each benchmark file creates
See examples/ for some simple code examples written in SCT.
The syntax is inspired by C, for a lot of basic statements.
The language has the following types:
int
float
void
Predicate
Important
The language does not have boolean values, instead any value
// I am a comment
// assignments
int x = 2;
float y = 5.0;
// boolean expressions
x = x < y;
// Operators: >, <, >=, <=, ==, !=, &&, ||
// binary operators
y = x + y;
// Operators: +, -, *, /, %
// other
y = !y; // unary NOT
y = -y; // unary minus
x = (int)y; // typecast - can only cast between int and float
if (<condition>) { <statements> }
// provide any amount of else if statements
else if (<condition>) { <statements> }
// and optionally an else statement
else { <statements> }
while (<condition>) { <statements> }
// an example function definition
// the arrow `->` indicates the return type.
// if not `void` return an expression using the `return` keyword.
function foo(int a, float b, Predicate c) -> void { <statements> }
species Foo(int n, float i) {
function seven() -> int {
return 7;
}
decorator Bar {
n = n + 1;
}
@Bar
state Baz {
destroy;
}
@Bar
state Qux {
float x = seven();
enter Baz;
}
}
This defines a new species with 2 fields: n
and i
.
n
and i
are only accessible within each instance of the species (agent), likewise with the function seven()
.
Decorators and states are not directly accessible by anyone, but rather executed by the SCT runtime.
The SCT type Predicate
is used to specify a pattern matching possible agents.
// the predicate syntax looks as follows:
<species>::<state/?>(<field>: <value>, ...)
Predicate a = Foo::Qux(); // match any agent Foo in state Qux
Predicate a = Foo::Baz(i: 4); // match any agent Foo in state Baz having i == 4
Predicate a = Foo::?(); // match any agent Foo in any state
Predicate a = Foo::?(n: -7, i: 1); // match any agent Foo having n == -7 and i == 1
Predicates can be used with the builtin functions exists()
and count()
.
exists(p)
returns 1 if there exists an agent fulfillingp
, 0 otherwise.count(p)
returns an int with the amount of agents matchingp
.
To create a new agent, use the create
keyword, followed by a fully qualified agent predicate.
This means that all fields must be specified, and the state cannot be the wildcard operator ?
.
An agent can exclude itself from the succeeding iterations (ticks) using the destroy
keyword.
To stop the simulation entirely, use the exit
keyword. This completes the current tick, and stops afterwards.
All states must specify an end-condition, these can be either enter
exit
or destroy
.
Decorators allows for less redundant code, as different states can have the same logic prepended to themselves.
It is also possible to use the end-conditions inside a decorator, which is not possible in a function.
Important
All SCT programs must include a setup()
function, which must take 0 arguments and return void
.
This function is automatically invoked before the simulation is run, to setup the start conditions for the simulation.
The language also includes a RNG, which is invoked with rand()
rand()
returns a float in the range [0,1[
It can also be seeded (to create reproducible simulations), using seed()
, which takes an int as argument.
Sometimes (mainly when debugging), it is benefitial to be able to print all the agents matching some predicate p
.
Or printing the amount of agents matching p
.
Both of these functionalities can be achieved with print(p)
and printCount(p)
respectively.
This is seen in the examples/FizzBuzz.sct
Lastly, there exists some syntactic sugar, to ease the process of writing some simulations:
#e
is a shorthand forcount(e)
e => i
is a sharthand forif (e) { enter i; }
The compiler includes a standard library, with the following functions:
exitAfterTicks(int x)
- exits the simulation after
x
ticks.
- exits the simulation after
exitWhenExists(Predicate p)
- exits the simulation when
p
is fulfilled by some agent.
- exits the simulation when
exitWhenNoLongerExists(Predicate p)
- exits the simulation when no agent fulfills
p
.
- exits the simulation when no agent fulfills
exitWhenMoreThan(Predicate p, int x)
- exits the simulation when more than
x
agents fulfillp
.
- exits the simulation when more than
exitWhenLessThan(Predicate p, int x)
- exits the simulation when less than
x
agents fulfillp
.
- exits the simulation when less than
The output of a simulation is a JSON object for each tick, ticks seperated by newlines. SCT does not provide a way to plot the output in any way, this is instead up to the user.
scripts/society-plot.jl does however, include an example of how to plot a simple simulation on a graph.