We’ve now toured Racket’s whole language-making apparatus. These steps are also summarized in the master recipe.
Every Racket-implemented language has two pieces: a reader and an expander.
The #lang line boots into our reader. The reader compiles the surface notation into S-expressions.
The reader exports a function called read-syntax. This function consumes tokens from the source file until it reaches eof. This function returns a module expression that has no bindings.
Question: how does Racket enforce that last part?
The reader often uses a helper function called a lexer to read tokens from the source file. It often uses a helper function called a parser to arrange those tokens into a parse tree.
The expander converts the parse tree from the reader into a valid Racket program.
The expander exports a macro called #%module-begin, which belongs to a set of special macros called interposition points. This macro is called first, and receives the parse tree as input.
The expander also exports bindings for every identifier in the parse tree. These bindings can represent either macros or functions.
This is the essential playbook for every language you will ever make in Racket. But it’s the starting point, not the end.
As long as your reader and expander conform to the conventions Racket expects, your language will still work. You control the details.
Allocating work among the lexer, parser, and expander is more art than science.
Today, it seems like a lot. With a little practice, it won’t. You’ll find yourself implementing quick little languages in one source file.
Since every language ultimately compiles into a Racket program, your language can do anything Racket can do.
Corollary: the more you learn about Racket and its libraries, the more possibilities you’ll see in LOP with Racket. For instance, drawing, database, web apps, GUI apps, etc.