This document explains how to edit, build, and run this project, both if you want to change your fork of the language or if you want to upstream improvements to the original language.
The easiest way to build the project is using cabal
, and the most commonly
used commands are:
$ cabal build exe:grace # Build the `grace` executable
$ cabal test # Run all tests
$ cabal test tasty # Faster: run only tasty tests
$ cabal test tasty --test-option=--accept # Update golden tests
$ cabal test doctest # Run only doctests
$ cabal haddock --hyperlink-source # Generate Haskell documentation
You can also enable coverage checking by running this step before running the tests:
$ cabal configure --enable-coverage
You'll probably also want to use ghcid
or
haskell-language-server
for interactive development.
This project also provides devShells
for Nix users, but it's not necessary
for project development.
The project tries to be as maintainable possible, meaning that most mistakes you make will be caught by the type-checker and you will only need to update a few places to make the most common changes. However, there are a few places that you need to remember to update and the type-checker won't remind you.
For example, any time you add a new language feature you will need to update the parser in order to actually use the feature, and nothing will automatically remind you to do that.
Generally speaking, if you're not sure where to begin then start by identifying
the most closely-related language feature and searching the codebase for all
occurrences of the matching constructor in the Syntax
tree.
If you want to make changes to the website you will need to build using GHCJS, which entails the following commands:
$ nix develop .#ghcjs
[nix-shell]$ cabal v1-configure --ghcjs --disable-tests
[nix-shell]$ cabal v1-build
… and if you want to test the website, then run the following additional command after each build:
[nix-shell]$ cp dist/build/try-grace/try-grace.jsexe/all.js website/js/all.min.js
… and then open ./website/index.html
in your
browser.
The test suite will not work and also ghcid
will not work when developing
using GHCJS.
To add a new built-in function, edit the Syntax
module to add a new
constructor to the Builtin
type, then fix all of the type errors, which should
consist of:
- Specifying the type of the builtin
- Specifying how to pretty-print the builtin
Then, edit the Normalize
module to change the apply
function to
add a case for handling the newly-added builtin.
CAREFULLY NOTE: If the built-in accepts a type T
as input (e.g. Real
)
then it also must accept all types that are subtypes of T
, too (e.g.
Integer
and Natural
, which are subtypes of Real
). For example, the
Integer/even
function has type Integer -> Bool
, so the apply
function
has to handle the case where the input to the Integer/even
function is either
an Integer
or a Natural
(since Natural
is a subtype of Integer
).
Finally, add support for parsing the built-in by:
- Adding a new
Token
for theBuiltin
in theLexer
module - Adding the built-in name to the
reserved
words in theLexer
module - Adding a new parsing rule in the
Parser
module.
Adding a new operator is basically the same as adding a new built-in, with
the main change being that you change the Operator
type within the
Syntax
module, instead of changing the Builtin
type.
The other difference is that you will change the Normalize
in a
different place (where all of the operator logic is).
To add a new scalar type, edit the Syntax
module to add a new
constructor to the Scalar
type (representing the scalar literal). Also, edit
the Monotype
module to add a new constructor to the Scalar
type
in that module (representing the corresponding scalar type). Then fix all of
the type errors, which will consist of:
-
Specifying how to prettyprint the scalar literal
-
Specifying how to prettyprint the scalar type
-
Specifying how to infer the type of the scalar literal
… by returning the matching scalar type you just created
Finally, edit the Lexer
and Parser
modules to lex and
parse the new scalar literal and scalar type you just created.
Probably the easiest way to add a new keyword is to study how an existing
keyword is implemented, such as the if
/ then
/ else
keyword. Search
the codebase for all occurrences of the If
constructor and follow the pattern.
Just like keywords, the easiest way to add a new complex type is to study how an
existing complex type is implemented, such as the List
type. Search the
codebase for all occurrences of the List
constructor and follow the pattern.
When adding new keywords or complex type you will need to take care to remember to update the pretty-printing logic. By default, the code will go into an infinite loop if you forget to do this, and this post explains the reason why:
You don't need to worry about this if you are adding new built-ins / operators / scalars, since those are already handled uniformly.
This is probably the hardest part of making any changes, especially changes that add new keywords or complex types, since they cannot be handled uniformly.
If you're new to logical notation in general, then I recommend first reading A tutorial implementation of a dependently typed lambda calculus, which explains the correspondence between logical notation and Haskell code.
Then read the
Complete and Easy Bidirectional Typechecking for Higher-Rank Polymorphism
paper which explains the general principles behind the type-checking algorithm.
However, there are a few nuances that are not obvious from a casual reading of
the paper, so follow up by reading the Infer
module, which is heavily
commented with things I had to figure out in the course of attempting to
implement the paper.