When we invoke #lang wires, Racket will look for a "main.rkt" module within our wires directory. Within that module, it will expect to find a reader submodule that provides our read-syntax function.
1 2 3 4 | #lang br/quicklang (module reader br/quicklang (provide read-syntax) ···) |
Because we’re building our language in one module, we’ll put the definition of read-syntax inside our reader submodule, rather than import it from elsewhere with require:
1 2 3 4 5 | #lang br/quicklang (module reader br/quicklang (provide read-syntax) (define (read-syntax path port) ···)) |
The guts of our read-syntax function will be similar to the one we made for stacker. There, we pulled each line from the source file and converted it into a parenthesized S-expression, also known as a datum, that looked like (handle ···).
This time, we’ll wrap each line with (wire ···). Then, in the expander, we’ll add a wire macro that expands this form.
Since the logic of read-syntax will be roughly the same as before, let’s take this opportunity to learn some neater notation:
1 2 3 4 5 6 7 8 | #lang br/quicklang (module reader br/quicklang (provide read-syntax) (define (read-syntax path port) (define wire-datums (for/list ([wire-str (in-lines port)]) (format-datum '(wire ~a) wire-str))) ···)) |
The first part of read-syntax will create our list of wire-datums. We make this list with for/list, which loops over its iterators and evaluates the body each time. Whereas plain for discards the results of these evaluations, for/list collects them into a list. + For more about these and other iteration expressions, see loops.
We iterate over the lines in our input port using in-lines, assigning each line to wire-str, which holds a string of code from the source file. Then we use format-datum to convert this string into a (wire ···) datum. The resulting list of datums is assigned to wire-datums.
Once we have our wire-datums from our source file, we need to insert them into code representing a module expression. Recall that the code for our module expression needs to be packaged as a syntax object without any bindings.
In stacker, we accomplished this by making a datum using quasiquote notation and then using datum->syntax to convert it into a syntax object.
This time, we’ll use quasisyntax notation to do both at the same time. Quasisyntax works the same way as quasiquote, but each operator is prefixed with #, and the result of each operator is a syntax object rather than a simple datum:
Similarly, if our module code would’ve looked like this as a raw quasiquoted datum:
1 2 | `(module wires-mod wires/main ,@wire-datums) |
We can convert it to quasisyntax like so:
1 2 | #`(module wires-mod wires/main #,@wire-datums) |
Which gives us the syntax object we need.
Well, almost. When we use #` (or the ordinary #' prefix) to make a syntax object, the lexical context—meaning, the currently available bindings—of the surrounding code will automatically be attached to the new syntax object. Usually, this is what we want: part of Racket’s policy of hygiene is that syntax objects should retain the lexical context from the location where they were created.
In this case, however, it’s not what we want. Why? Because read-syntax has a special responsibility to return a syntax object without bindings. So as the last step, we’ll pass our syntax object to strip-bindings, which removes them.
Our updated read-syntax:
1 2 3 4 5 6 7 8 9 10 | #lang br/quicklang (module reader br/quicklang (provide read-syntax) (define (read-syntax path port) (define wire-datums (for/list ([wire-str (in-lines port)]) (format-datum '(wire ~a) wire-str))) (strip-bindings #`(module wires-mod wires/main #,@wire-datums)))) |
When we’re working on Racket code in DrRacket, it’s convenient to be able to try out quick examples and tests on the REPL. Let’s try testing our read-syntax now, using the test-reader function included in br.
If we use the sample wire code "123 AND g -> ac", we expect to get (wire 123 AND g -> ac), wrapped in a module expression. Let’s see what actually happens:
1 | (test-reader read-syntax "123 AND g -> ac") |
1 | 123
|
Whoops—that’s wrong. But the problem is not a defect in our code. Rather, the problem is that our new read-syntax is stashed inside a submodule, where it’s invisible to the REPL. So right now, when we say read-syntax on the REPL, we’re actually invoking the read-syntax that’s part of br/quicklang.
How can we get the read-syntax we want? One way is to explicitly require our submodule on the REPL:
1 2 | (require (submod wires reader)) (test-reader read-syntax "123 AND g -> ac") |
This time, we’ll get the right result:
1 | '(module wires-mod wires/main (wire 123 AND g -> ac)) |
This works. But it’s cumbersome.
There is a better way: rather than declaring our reader submodule with module, we’ll use module+. As its name suggests, rather than create a fully independent submodule, module+ makes a submodule that picks up all the definitions in the surrounding module. To the outside world, the reader submodule will work the same way.
But inside our module, we can move our read-syntax outside the reader submodule, so it can be available on the REPL. We update the code like so:
1 2 3 4 5 6 7 8 9 10 11 12 | #lang br/quicklang (module+ reader (provide read-syntax)) (define (read-syntax path port) (define wire-datums (for/list ([wire-str (in-lines port)]) (format-datum '(wire ~a) wire-str))) (strip-bindings #`(module wires-mod wires/main #,@wire-datums))) |
Like module, a submodule declared with module+ has a submodule name. But unlike module, it doesn’t have a module path for its initial imports. That’s because it doesn’t need one—it’s already getting its initial imports from the surrounding module.
This time, when we use test-reader on the REPL:
1 | (test-reader read-syntax "123 AND g -> ac") |
We’ll get the right result, because our new read-syntax is visible to the REPL:
1 | '(module wires-mod wires/main (wire 123 AND g -> ac)) |
Our reader is complete. BTW, though we could’ve gotten by without using module+, it has a couple other useful features that we’ll rely on as we write the expander.