Now that we have a taste for bf programming, we might want to use our interpreter more freely. The #lang reader "reader.rkt" bootstrapping only works for source files that are stored in the same directory. What a drag. We’d rather be able to invoke our language within a source file anywhere as #lang bf.
To do this, we need to install our code as a . A package is Racket’s basic unit of installable software. The #lang notation at the top of a source file cooperates with the package system: when we write #lang name, Racket tries to find name among our installed packages. (Conversely, until we install our language as a package, we have to keep using the #lang reader path form.)
Here’s how we install our project as package:
We create a directory for our package. For now, the name of the directory needs to match what we use in the #lang name form. Since we want to invoke our interpreter as #lang bf, we’ll call our package directory bf. + It’s possible to make the package name and the #lang name different. We’ll see how to do this in a later tutorial. Create this directory somewhere convenient.
We move our three modules—"parser.rkt", "reader.rkt", and "expander.rkt"—into this new bf directory.
When we invoke #lang bf, Racket will look for a "main.rkt" module in the bf directory. In that module, it will expect to find a reader submodule that provides our read-syntax function. If we wanted, we could put our actual read-syntax code there. But it’s just as easy to just use require and provide within the reader submodule:
We also need to adjust our read-syntax function in "reader.rkt". The module code returned by this function refers to "expander.rkt". This is a relative path string, which won’t be valid once we start making bf source files in other directories. Instead, we reference the expander using its new package name, bf/expander (as a module path, it loses its .rkt extension):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#lang br/quicklang (require "parser.rkt") (define (read-syntax path port) (define parse-tree (parse path (make-tokenizer port))) (define module-datum `(module bf-mod bf/expander ,parse-tree)) (datum->syntax #f module-datum)) (provide read-syntax) (require brag/support) (define (make-tokenizer port) (define (next-token) (define bf-lexer (lexer [(char-set "><-.,+") lexeme] [any-char (next-token)])) (bf-lexer port)) next-token)
By the way, we could change the other module references in our code to use this new package notation. For instance, in "reader.rkt" we could require bf/parser rather than "parser.rkt". But it’s unnecessary, because those files will remain in the same directory, so they can use local path strings.
Finally, we use Racket’s command-line program raco to install our package. In a terminal window, we’ll move into our new bf directory and do raco pkg install:
> cd path/to/bf > raco pkg install
This will trigger a flurry of status messages as Racket updates our installation.
If there are no errors, we can go back to DrRacket and test that our package works by writing a #lang bf program:
1 2 3
#lang bf Greatest language ever! ++++++++[>++++++++<-]>.
If we ever want to delete the package, we can do
> raco pkg remove bf
If we want to rename the package, we can delete it, rename the directory, update the name of the expander in read-syntax, and then reinstall it using the procedure above.
We waited until the end of our bf project to install it as a package. Could we have done this at the beginning? Sure. Once we install a language project as a package, we can still work on the modules inside. These changes are reflected immediately when we invoke the language, without needing to run raco pkg install again. And we can use the #lang name notation. Thus, the benefits of creating a package outweigh the extra housekeeping.
We’ve only scratched the surface of packages. In a later tutorial, we’ll see how we can add documentation and tests to a Racket package, and distribute it to others using Racket’s online package server.