This file explains how to develop the Jou compiler.
If you have any trouble with this, please create an issue!
Linux
Following the instructions in the README is enough.
To edit the C code, you can use any editor that uses clangd
.
The make
command creates a file compile_flags.txt
to help clangd
find the LLVM header files.
64-bit Windows
- Download and install Git from Git's website if you don't have it already.
- Open Git Bash from the start menu.
You must use Git Bash for running bash scripts such as
windows_setup.sh
andruntests.sh
. - Clone the project with the command prompt:
You can put the project anywhere. The above command places it on the desktop.
cd Desktop git clone https://github.com/Akuli/jou
- Run a script that does the rest of the setup for you:
If you have a slow internet connection and it takes a long time for
cd jou ./windows_setup.sh
windows_setup.sh
to download mingw64, you can instead run./windows_setup.sh --small
. This way it usesmingw64-small.zip
, which is just like the usual mingw64, but with many large files deleted to make it smaller. I created it locally on my computer. If you don't want to trust it, you can runwindows_setup.sh
without--small
or look at how.github/workflows/windows.yml
comparesmingw64-small.zip
to the originalmingw64.zip
. - Compile Jou:
The
source activate mingw32-make
source activate
command addsC:\Users\YourName\Desktop\jou\mingw64\bin
to your PATH, whereC:\Users\YourName\Desktop
is the folder where you cloned Jou. If you don't want to run it every time you open a Git Bash window to work on Jou, you can instead add it to your PATH permanently with Control Panel. - Compile and run hello world:
You should see
./jou.exe examples/hello.jou
Hello World
printed. If you instead get errors about missing DLL files, runsource activate
first. The Jou compiler depends on DLLs inmingw64\bin
, somingw64\bin
must be in PATH when running it. - Run tests:
./runtests.sh
The compiler is currently written in C. At a high level, the compilation steps are:
- Tokenize: split the source code into tokens
- Parse: build an Abstract Syntax Tree from the tokens
- Typecheck: errors for wrong number or type of function arguments etc, figure out the type of each expression in the AST
- Build CFG: build Control Flow Graphs for each function from the AST
- Simplify CFG: simplify and analyze the control flow graphs in various ways, emit warnings as needed
- Codegen: convert the CFGs into LLVM IR
- Invoke
clang
and pass it the generated LLVM IR
To get a good idea of how these steps work, you can run the compiler in verbose mode:
$ ./jou -v examples/hello.jou # High-level overview
$ ./jou -vv examples/hello.jou # Show all details
With -vv
(or --verbose --verbose
), the compiler shows
the tokens, AST, CFGs and LLVM IR generated.
The control flow graphs are shown twice, before and after simplifying them.
Similarly, LLVM IR is shown before and after optimizing.
After exploring the verbose output, you should probably
read src/jou_compiler.h
and have a quick look at src/util.h
.
GitHub Actions runs all tests when you make a pull request, so you don't need to run tests locally if you only intend to fix a couple small things. That said, test-driven development works very well for developing compilers. There should be a test (or a TODO comment about adding a test) for every feature and for every compiler error/warning.
Running tests (if on Windows, use Git Bash):
$ ./runtests.sh
The runtests.sh
script does a few things:
- It compiles the Jou compiler if you have changed something in
src/
since the last time it was compiled. - It runs all Jou files in
examples/
andtests/
. To speed things up, it runs two files in parallel. - It ensures that the Jou files output what is expected.
The expected output is auto-generated from comments in the Jou files:
- A comment like
# Output: foo
appends a linefoo
to the expected output. - A comment like
# Error: foo
on line 123 of filetests/bar/baz.jou
appends a linecompiler error in file "tests/bar/baz.jou", line 123: foo
. - A comment like
# Warning: foo
on line 123 of filetests/bar/baz.jou
appends a linecompiler warning for file "tests/bar/baz.jou", line 123: foo
. - Files in
examples/
andtests/should_succeed/
should run successfully (exit code 0). All other files should cause a compiler error (exit code 1).
If the actual output doesn't match the expected output, you will see diffs where
green (+) is the program's output and red (-) is what was expected.
The command that was ran (e.g. ./jou examples/hello.jou
) is shown just above the diff,
and you can run it again manually to debug a test failure.
You can also put e.g. valgrind
or gdb --args
in front of the command.
Because running tests is slow, you often want to run only one test, or only a few tests.
For example, maybe you want to run all Advent of Code solutions.
To do things like this, the test script takes a substring of the file path as an argument,
and runs only tests whose path contains that substring.
For example, ./runtests.sh aoc
finds files like examples/aoc2023/day03/part2.jou
.
$ ./runtests.sh aoc # run Advent of Code solutions
$ ./runtests.sh class # run tests related to defining classes
$ ./runtests.sh ascii_test # run tests for the "stdlib/ascii.jou" module
You can use --verbose
to see what test files get selected:
$ ./runtests.sh ascii_test --verbose
To find missing free()
s and various other memory bugs,
you can also run the tests under valgrind
(but this doesn't work on Windows, because valgrind doesn't support Windows):
$ sudo apt install valgrind
$ ./runtests.sh --valgrind
This doesn't do anything with tests that are supposed to fail with an error, for a few reasons:
- The compiler does not free memory allocations when it exits with an error.
This is fine because the operating system will free the memory anyway,
but
valgrind
would see it as many memory leaks. - Valgrinding is slow. Most tests are about compiler errors,
and
make valgrind
would take several minutes if they weren't skipped. - Most problems in error message code are spotted by non-valgrinded tests.
There are also a few other ways to run the tests.
You can look at .github/workflows/
to see how the CI runs tests.
To ensure that documentation stays up to date, it is also possible to run code examples in the documentation as tests:
$ ./doctest.sh
The doctest.sh
script finds code examples from markdown files in doc/
.
It only looks at code examples that contain # Output:
, # Warning:
or # Error:
comments.
It then attempts to run each example and compares the output similarly to runtests.sh
.
Using Jou on Linux is easy,
because you can install Jou's dependencies with apt
(or whatever package manager you have).
To make using Jou on Windows equally easy, or at least as painless as possible,
users can download a zip file that contains everything needed to run Jou.
See the README for instructions to install Jou from a zip file.
A zip file is built and released in GitHub Actions every morning at 4AM UTC, or when triggered manually from GitHub's UI (e.g. to release an important bugfix as quickly as possible). No new release is made if there are no new commits.
Some parts of the build are done in .github/workflows/windows.yml
,
and the rest is in .github/workflows/release.yml
.
The difference is that windows.yml
runs on every pull request and on every push to main
,
but release.yml
runs daily and when triggered manually.
This means that:
windows.yml
should do most of the build. It should also run tests on the build results to make sure that everything works. If something in this file stops working, it will be noticed very quickly when someone makes a pull request.release.yml
should be simple and small. Ifrelease.yml
breaks, it might take a while for someone to notice it. Ideally it would only download the build results ofwindows.yml
and create a release.
There is also jou --update
.
On Linux it simply runs git pull
and recompiles the Jou compiler.
On Windows it runs a PowerShell script that downloads and installs the latest release from GitHub Actions.