Skip to content

Debugging

Siddhartha Kasivajhula edited this page Jan 27, 2024 · 2 revisions

There are many strategies for debugging while working in the Qi codebase. Here are a few (please contribute your own!).

REPL

You can always "run" a buffer and then interact with its runtime in the REPL. This is a standard workflow in Racket Mode (see racket-run). Once you've done this, you can also interactively evaluate definitions in source buffers that are "sent" to the REPL for evaluation, as an alternative to typing things out directly in the REPL.

Scratch Buffer

DrRacket encourages a scratch buffer workflow where you write our your requires and any code you want to test, and then run the entire scratch buffer wholesale after each change, instead of evaluating expressions one at a time in the more REPL-oriented workflow encouraged by Emacs. This is a great way to try out small programs in a clean environment that is constructed from scratch each time and that avoids some common development mistakes. It also allows you to develop your exploratory code (which may be for testing or debugging purposes) incrementally. If you are using Emacs, you can get a similar workflow by using Mindstream (disclaimer: still unreleased and in preview).

Unit tests

Unit tests can help you identify the source of a bug as well as form the foundation for your attempt to fix such bugs.

Qi has almost 100% test coverage, and has unit tests at the language level as well as dedicated tests for the expander, and for each pass of the compiler as well as the whole cycle of compilation. This provides a great pipeline of tests that test each component of the language at different levels of abstraction. Becoming familiar with these tests will be incredibly useful as they can help you isolate bugs.

See the Source Code Overview of the qi-test package for an introduction to the test modules. Familiarize yourself with the Makefile targets that allow you to run different sets of tests. Depending on the results of these different sets of tests, you can triangulate where a bug might be and it can greatly narrow your search.

If you encounter a bug, writing a failing test to reproduce it is a great foundation for your workflow as you fix the issue (see racket -y below for more on this). This also then serves as a great regression test to detect the bug again in the future.

A useful test validates just one thing, failing when there is a problem with that thing, and passing otherwise. This helps us understand exactly what the problem is when there is a test failure, sparing us the need to study the test in detail.

Racket -y

If you make a change in a library module, you could run make build followed by make test to validate the fix. But due to Racket's separate compilation guarantee, you could just run the appropriate module (such as a test module) using racket -y, which tells Racket, "(y)es, please recompile all dependencies in running this module." This does the minimum work necessary to run the module and is typically much faster than going through the entire development loop.

Clone this wiki locally