7 Whole Languages
Throughout the tutorial, we have appealed to predefined elements of Rhombus as examples of the kind of extensibility that Rhombus enables. The difference between Rhombus functionality and the kinds of macros that we have written is that Rhombus forms are packaged into a language that you can use with #lang.
Packaging a set of macros into a form for use with #lang in the same way as rhombus requires two steps:
defining a module that exports all of the bindings that are in the language, including implicit forms; and
making the module part of your Racket installation, typically through a package, since the name after #lang is resolved to a implementing module through the installation.
We’ll consider the first step here, but we’ll leave details of the second step to the Rhombus documentation.
7.1 Modules and Macros
Each Rhombus source file that starts with #lang rhombus defines a module. A module can export bindings for use in other modules using export, and other modules reference the exporting module using import. In the case of modules that are adjacent in the filesystem, they can refer to each other via relative-path strings.
For example, if "fib.rhm" contains
"fib.rhm"
fib
fun fib(n):
match n
| 0 || 1: 1
then "main.rhm" in the same directory can use "fib.rhm" like this:
"main.rhm"
"fib.rhm" open
fib(5)
Macros can be exported just the same as functions. For example, if you start with the solution my_operator_soln.rhm to an earlier exercise, then you can use my_operator as exported from that module:
"main.rhm"
"my_operator_soln.rhm" open
my_operator a <!!!> b:
#true <!!!> #false
The my_operaor macro expands to a use of expr.macro, which is not directly accessible in "main.rhm", since it does not import rhombus/meta or use #lang rhombus/and_meta. the import my_operaor macro works, anyway, because syntax objects retain scope information and enable hygienic macros.
7.2 Modules and Spaces
Suppose that we want to use interp and prog of interp_space.rhm (from an earlier exercise) in "main.rhm":
"main.rhm"
"interp_space.rhm" open
interp(prog: 1 + 2,
{})
Clearly, we’ll need to adjust "interp_space.rhm" to export interp and prog:
interp
prog
This turns out not to be enough, however. After adding this export, attempting to run "main.rhm" reports an error about a missing #%literal for the 1 in prog: 1 + 2. The missing #%literal is in the one in the lc space. It will turn out that the + for lc is also not available.
To fix the problem, put an additional export form after the definition of the lc space "interp_space.rhm" (about 2/3 of the way down from the top of the file):
only_space lc:
+
==
let
fun
#%call
#%parens
#%literal
This example illustrates how Rhombus provides fine-grained control over the bindings that are accessible from a module. To be able to import these bindings, however, we needed at least import from the rhombus language. By creating a module that can be referenced through a #lang line, we can create a language that is smaller than rhombus.
7.3 Modules and Languages
We will not be able to write #lang "interp_space.rhm", because quoted relative paths are not allowed after #lang. Fortunately, the shrubbery language can give us a little help. When #lang shrubbery is by itself on a line, then it works the way we used in an earlier exercise. But when shrubbery is followed by a quoted path as in #lang shrubbery "interp_space.rhm", then it imports the path for use in the module body, instead of just printing the body’s parsed shrubbery form.
Our goal is to make this "main.rhm" module work by using the prog parser and interp function from "interp_space.rhm" instead of compiling it as Rhombus code. Again, writing an interpreter is not really the best way to create a language in Rhombus. You should just compile your language directly to Rhombus code via macros. We continue to use the interpreter example here, anyway.
"main.rhm"
f(x) * 3
Attempting to run this new "main.rhm" will fail, however, with an error message about a missing #%module-begin binding. As the #% prefix suggests, #%module-begin is an implicit that wraps a module body. It turns out that #%module-begin is from the Racket layer of Rhombus, and it works in Racket-native terms. Rhombus has its own #%module_block protocol that works in Rhombus-native terms, and Rhombus exports a #%module-begin that bridges to #%module_block. The Rhombus spelling of the Racket #%module-begin identifier (which has a hyphen) is #{#%module-begin}.
So, to make the new "main.rhm" work, we need to change "interp_space.rhm" to
re-export #{#%module-begin} from rhombus; and
define and export a #%module_block declaration form that expands to use interp and prog.
Here’s the decl.macro and export form to add to "interp_space.rhm":
#{#%module-begin}
my_module_block as #%module_block
decl.macro 'my_module_block:
$body':
interp(prog: $body,
{})'
The exported #%module_block macro is defined as my_module_block so that it can expand to a use of the Rhombus #%module_block form, and then it is renamed to #%module_block on export.
7.4 Exercise
Change the my_module_block form in "interp_space.rhm" so that it allows multiple ‹group›s in the module body, and it independently interps and prints a result for each of them.