Program result = read → expand → evaluate → print.
The reader for the language converts the surface notation of the source file into S-expressions and returns a a new module expression.
For this reason, every source file starts with a #lang line (pronounced hash-lang) that “boots” into the reader.
The module is the basic organizational unit of code in Racket. A module expression includes a name, an expander, and a body containing other S-expressions:
(module module-name expander
other-expressions
to-expand
and-evaluate ...)
Modules can also contain other modules. These are called, unsurprisingly, submodules, and they are loaded independently of their enclosing module:
(module module-name expander
other-expressions
to-expand
and-evaluate ···
(module submodule-name submodule-expander
more-expressions ···))
Every Racket source file consists of a single module expression at the top level. This is why in Racket lingo, a source file is also known as a module. For instance, these two Racket sources are roughly equivalent:
#lang racket
(displayln (format "Hello world ~a" 42))
(module foo racket
(displayln (format "Hello world ~a" 42)))
Question: in what way are they not equivalent?
The expander determines the semantics of the new module. It does this by supplying the initial set of bindings for the module expression returned by the reader, thereby determining the meaning of the identifiers within the S-expressions.
(module foo pollen
(println metas))
(module foo racket
(println metas))
Question: why do the modules above behave differently?
The expander leaves behind an ordinary Racket program (known as the fully expanded program) which can be evaluated normally (by running it with racket).
Our goal in every language implementation: to use the reader and expander to convert our surface notation into the equivalent Racket program. In that sense, every Racket-implemented language is a source-to-source compiler.
Corollary: since every language ultimately compiles into a Racket program, your language can do anything Racket can do.
The reader and expander do their work in separate passes. How we divide the labor between them is discretionary.
A language can have multiple readers that target a single expander.
Question: why might this be valuable?
Or multiple expanders that target a single reader.
Question: why might this be valuable?
A language can be implemented using any #lang (as long as it adheres to a few necessary conventions).
Question: why might this be valuable?
Using #lang racket, write a program that behaves the same way as this Pollen program:
#lang pollen
Hello world
◊metas
Result when run:
Hello world
'#hasheq((here-path . "hello.rkt"))
And then at the REPL:
> doc
"Hello world\n'#hasheq((here-path . \"hello.rkt\"))"
All roads lead to #lang racket.