Grits is a type-checker and interpreter for intuitionistic session types written in Go, based on the semi-axiomatic sequent calculus.
You need to install the Go language. Then, build the project as follows:
go build .
This produces an executable grits
file, which can be used to typecheck and run programs ./grits path/to/file.grits
.
You can find some examples in the examples
directory.
./grits examples/nat_double.grits
Build in Windows
The go build .
command works similarly on Windows. In this case, a grits.exe
executable file is produced, which can be used as grits.exe <flags> <file>
. Example usage:
grits.exe examples/hello.grits
Run using docker
An alternative way to build and run Grits is via Docker.
-
You should have a Docker runtime installed. Installation instructions are available from https://www.docker.com. Ensure that the Docker daemon is running.
-
Create a docker image tagged
grits
usingdocker-build.sh
:chmod +x docker-build.sh ./docker-build.sh
Or use
docker build -t grits:latest .
directly. -
To run the docker image tagged
grits
, use the command./docker-run.sh <flags> <file>
(might need to runchmod +x docker-run.sh
). -
For instance, to typecheck and execute
hello.grits
, use:./docker-run.sh examples/hello.grits
The tool supports various flags for customization:
--notypecheck
: skip typechecking--noexecute
: skip execution--sync
,--async
: execute using synchronous or asynchronous semantics (default:--async
)--verbosity <level>
: control verbosity (1 is the least verbose, 3 is the most)
To benchmark a specific program, use the --benchmark
flag. Optional flags include --maxcores <number of cores>
and --repeat <number of times>
for fine-tuning tests. All results are stored in the benchmark-results/
directory created during benchmarking. Example usage:
./grits --benchmark examples/nat_double.grits
./grits --benchmark --maxcores 1 --repeat 5 examples/nat_double.grits
To run the pre-configured sample benchmarks: ./grits --sample-benchmarks
.
For detailed information on performance evaluation, refer to benchmarks/readme.
The following program defines a type nat
(representing natural number), a function double
and initiates two processes.
type nat = +{zero : 1, succ : nat}
let double(x : nat) : nat =
case x (
zero<x'> => self.zero<x'>
| succ<x'> => h <- new double(x');
d : nat <- new self.succ<h>;
self.succ<d>
)
// Initiate execution
prc[d0] : nat =
t : 1 <- new close self;
z : nat <- new self.zero<t>;
self.succ<z>
prc[b] : nat =
d1 <- new double(d0);
d2 <- new double(d1);
fwd self d2
The full grammar accepted by our compiler is as follows:
<prog> ::= <statement>*
<statement> ::= type <label> = <type> // labelled session type
| let <label> ( [<param>] ) : <type> = <term> // function declaration
| let <label> '[' <param> ']' = <term> // function declaration with explicit provider name
| assuming <param> // add name type assumptions
| prc '[' <name> ']' : <type> = <term> // create processes
| exec <label> ( ) // execute function
<param> ::= <name> : <type> [ , <param> ] // typed variable names
<type> ::= [<modality>] <type_i> // session type with optional modality
<type_i> ::= <label> // session type label
| 1 // unit type
| + { <branch_type> } // internal choice
| & { <branch_type> } // external choice
| <type_i> * <type_i> // send
| <type_i> -* <type_i> // receive
| <modality> /\ <modality> <type_i> // upshift
| <modality> \/ <modality> <type_i> // downshift
| ( <type_i> )
<branch_type> ::= <label> : <type_i> [ , <branch_type> ] // labelled branches
<modality> ::= r | rep | replicable // replicable mode
| m | mul | multicast // multicast mode
| a | aff | affine // affine mode
| l | lin | linear // linear mode
<term> ::= send <name> '<' <name> , <name> '>' // send names
| '<' <name> , <name> '>' <- recv <name> ; <term> // receive names
| <name> . <label> '<' <name> '>' // send label
| case <name> ( <branches> ) // receive label
| <name> [ : <type> ] <- new <term>; <term> // spawn new process
| <label> ( [<names>] ) // function call
| fwd <name> <name> // forward name
| '<' <name> , <name> '>' <- split <name> ; <term> // split name
| close <name> // close name
| wait <name> ; term // wait for name to close
| cast <name> '<' <name> '>' // send shift
| <name> <- shift <name> ; <term> // receive shift
| print <label> ; <term> // output label
| ( <term> )
<branches> ::= <label> '<' <name> '>' => <term> [ '|' <branches> ] // term branches
<names> ::= <name> [ ',' <names> ] // list of names
<name> ::= 'self' // provider channel[s]
| <channel_name> // channel name
| <polarity> <channel_name> // channel with explicit polarity
<polarity> ::= + // positive polarity
| - // negative polarity
Others:
<label> is an alpha-numeric combination (e.g. used to represent a choice option)
// Single line comments
/* Multi line comments */
whitespace is ignored
This project requires Go version 1.21 (or later).
To build the project use go build .
. To execute tests, run go test ./...
.
There are three main components.
- Parser - Parses a program into a list of types, functions and processes (refer to
parser/parser.go
). - Typechecker - Typechecks a programs using intuitionistic session types (refer to
process/typechecker.go
andtypes/types.go
). - Interpreter - Programs are executed using either the non-polarized transition semantics (v1,
process/transition_np.go
) or the (async) polarized version (v2,process/transition.go
).
Some other notable parts:
- The entry point can be found in
main.go
. Cli commands are parsed incmd/cli.go
. process/runtime.go
: Entry point for the interpreter. Sets up the processes, channels and monitor before initiating execution.process/form.go
: contains the different forms that a process can take. They are used to create the AST of processes.webserver/web_server.go
: provides an external interface to compile and execute a program via a webserver (refer to the docs) (wip)