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.gritsBuild 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.gritsRun 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
gritsusingdocker-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.gritsTo 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.goandtypes/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)