Having figured out imports, let’s go the other direction and add exports to BASIC. (Again, not something that was ever part of the original language. Don’t panic.)
How could this work? Let’s suppose we have a BASIC program that defines variables and functions (temporarily using basic-demo-3 as the language, just to see how it works):
1 2 3 4 5 6 7 8 | #lang basic-demo-3 10 def div(num, denom) = num / denom 20 x = 5 : y = 10 30 print x - 4 40 x = 15 : y = 30 50 print div(y, x) 60 x = 20 70 print div((x + x + x), x) |
The result of this program:
1 2 3 | 1 2 3 |
Now let’s suppose we want to make the function div and the variable x available to other programs. To do this, we add two export statements (to the end of the program, but as with import, the location shouldn’t matter):
1 2 3 4 5 6 7 8 9 | #lang basic-demo-3 10 def div(num, denom) = num / denom 20 x = 5 : y = 10 30 print x - 4 40 x = 15 : y = 30 50 print div(y, x) 60 x = 20 70 print div((x + x + x), x) 80 export div : export x |
This program will still run the same way:
1 2 3 | 1 2 3 |
But now, if we import this "sample-exporter.rkt" module into another Racket program using require, we can use div as a function and x as a value:
1 2 3 4 5 | #lang br (require basic/sample-exporter) div x (div x 10) |
1 2 3 | #<procedure:div> 20 2 |
Let’s also notice what’s not happening when we import "sample-exporter.rkt": though the BASIC program inside is running, its print statements don’t produce any results on the REPL.
To make exports work, we have two tasks:
We need to find the the export statements in the program and convert them to top-level provide forms.
We need to suppress the output when the program is being used with require or other importing form.
For the first task, we can run the same play we used for imports. We’ll use the parser to mark our exported names with new parser rule—let’s call it b-export-name. We’ll splice this rule so that the export names end up carrying a syntax property called 'b-export-name. We’ll use this syntax property to find the names in our parse tree. Then we can put them inside a provide form at the top level of our module. We’ll also need to convert the b-export statement itself into a (void) form.
The second task—suppressing the output of the program—presents a new kind of problem. How do we change the behavior of print statements in certain conditions?
Let’s zoom out so we can see the bigger picture. Our task is a particular version of a more general problem: how to change the behavior of a module when it’s run directly vs. when it’s imported into another module.
We first encountered this problem in wires. In that case, we needed to set up the language so that it printed certain results after the wire functions were defined. To do this, we used the main submodule, a special submodule that Racket only invokes when the module is run directly, but not when it’s imported.
Sounds promising. Can a main submodule help us here? Not quite. The limitation of a main submodule is that it’s only invoked after the rest of the module has run. In wires, that worked fine, because we wanted to print some results after certain functions were defined in the body of the module.
In BASIC, however, the print statements can be mixed anywhere among the function and variable definitions in the source. There’s no way for us to rearrange the order of statements without changing the way the program works.
Suppose we had the idea to collect the result of every print statement into a buffer list of strings, and then use a main submodule to display that list at the end of the program. Not a bad plan, until we try to run this program:
1 2 3 | #lang basic-demo-3 10 print "HELLO WORLD!" 20 goto 10 |
In this case, the main submodule would never run, because the rest of the program never terminates. So there would be no output at all. That’s wrong.
What we’re looking for is a submodule that, like the main submodule, only runs when its enclosing module is run directly, and not when it’s imported. But unlike main, we want this submodule to be invoked before the rest of the module, so we can set up print to work a different way.
Fortunately, this option exists: it’s called the configure-runtime submodule.
To avoid confusion, let’s concede that configure-runtime is a misleading name. Throughout Racket, the term “run time” refers to the main phase of module evaluation. So no matter whether a module is imported, or run directly, it always has a run-time evaluation phase.
Contrary to this terminology, the configure-runtime submodule is only invoked when its enclosing module is run directly. Really, it would’ve been better named configure. But—we’ll live with it.
To use the configure-runtime submodule, we simply add it to the top level of our language module. In our case it’ll look like this:
1 2 3 4 5 | #'(#%module-begin (module configure-runtime br (require basic/setup) (do-setup!)) ···) |
Although the submodule can do the setup tasks itself, usually it loads another module (like basic/setup) and invokes a setup function (like do-setup!). There’s no magic to the names of the setup module or function. What matters is the configure-runtime name on the submodule.
The configure-runtime submodule has to use a full module declaration rather than module+. Why? Because module+ assumes that the surrounding module has been invoked, and builds atop it. Since the configure-runtime submodule runs before the other code, it needs a full module declaration so it can run independently.
With that background, let’s get into the code.
Very simple—we just add our new export keyword to the list of reserved-terms:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #lang br (require brag/support) (define-lex-abbrev digits (:+ (char-set "0123456789"))) (define-lex-abbrev reserved-terms (:or "print" "goto" "end" "+" ":" ";" "let" "=" "input" "-" "*" "/" "^" "mod" "(" ")" "if" "then" "else" "<" ">" "<>" "and" "or" "not" "gosub" "return" "for" "to" "step" "next" "def" "," "import" "export")) (define-lex-abbrev racket-id-kapu (:or whitespace (char-set "()[]{}\",'`;#|\\"))) (define basic-lexer (lexer-srcloc ["\n" (token 'NEWLINE lexeme)] [whitespace (token lexeme #:skip? #t)] [(from/stop-before "rem" "\n") (token 'REM lexeme)] [(:seq "[" (:+ (:~ racket-id-kapu)) "]") (token 'RACKET-ID (string->symbol (trim-ends "[" lexeme "]")))] [reserved-terms (token lexeme lexeme)] [(:seq alphabetic (:* (:or alphabetic numeric "$"))) (token 'ID (string->symbol lexeme))] [digits (token 'INTEGER (string->number lexeme))] [(:or (:seq (:? digits) "." digits) (:seq digits ".")) (token 'DECIMAL (string->number lexeme))] [(:or (from/to "\"" "\"") (from/to "'" "'")) (token 'STRING (substring lexeme 1 (sub1 (string-length lexeme))))])) (provide basic-lexer) |
We add a new b-export rule to handle the export statement, and add b-export to the right side of the b-statement rule.
We also mark the export statement argument with a spliced b-export-name rule. In the parse tree, this will be attached to the argument as a syntax property:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #lang brag b-program : [b-line] (/NEWLINE [b-line])* b-line : b-line-num [b-statement] (/":" [b-statement])* [b-rem] @b-line-num : INTEGER b-rem : REM @b-statement : b-end | b-print | b-goto | b-let | b-input | b-if | b-gosub | b-return | b-for | b-next | b-def | b-import | b-export b-end : /"end" b-print : /"print" [b-printable] (/";" [b-printable])* @b-printable : STRING | b-expr b-goto : /"goto" b-expr b-let : [/"let"] b-id /"=" (STRING | b-expr) b-if : /"if" b-expr /"then" (b-statement | b-expr) [/"else" (b-statement | b-expr)] b-input : /"input" b-id @b-id : ID b-gosub : /"gosub" b-expr b-return : /"return" b-for : /"for" b-id /"=" b-expr /"to" b-expr [/"step" b-expr] b-next : /"next" b-id b-def : /"def" b-id /"(" b-id [/"," b-id]* /")" /"=" b-expr b-import : /"import" b-import-name @b-import-name : RACKET-ID | STRING b-export : /"export" b-export-name @b-export-name : ID b-expr : b-or-expr b-or-expr : [b-or-expr "or"] b-and-expr b-and-expr : [b-and-expr "and"] b-not-expr b-not-expr : ["not"] b-comp-expr b-comp-expr : [b-comp-expr ("="|"<"|">"|"<>")] b-sum b-sum : [b-sum ("+"|"-")] b-product b-product : [b-product ("*"|"/"|"mod")] b-neg b-neg : ["-"] b-expt b-expt : [b-expt ("^")] b-value @b-value : b-number | b-id | /"(" b-expr /")" | b-func b-func : (ID | RACKET-ID) /"(" b-expr [/"," b-expr]* /")" @b-number : INTEGER | DECIMAL |
As we did in the b-func rule for functions, we’re deliberately using an ID in our b-export-name pattern rather than a b-id. Why? Because otherwise, an export statement would trigger the default definition of an identifier. In that case, a program like this wouldn’t raise an error:
1 2 | #lang basic 10 export x |
Here, the variable x would be created with the default value of 0, and that would be the exported value.
Instead, we’d like to mimic the behavior of a Racket program that tries to provide an undefined identifier:
1 | (provide x) |
1 | module: provided identifier not defined or imported for phase 0 in: x |
By using ID in the rule pattern rather than b-id, we can make this happen.
Let’s stub out our new "setup.rkt" module that will be invoked from our configure-runtime submodule:
Before we can proceed, we’ll have to get more precise about how to suppress our print statements when the module is run directly.
We know that every print statement expands into a call to displayln. (See "basic/misc.rkt".) In turn, displayln sends its output to the current-output-port, which is a Racket parameter that holds a reference to the port where things get printed. + To use a Unix analogy, current-output-port is like stdout. In fact, current-output-port often points to stdout.
Parameters are a special class of items in Racket that approximate the role of global variables. They’re used sparingly, usually for system-wide settings that would otherwise be difficult to reach.
Unlike global variables, parameters are functions. To read the value of a parameter, we invoke it as a function. To set the value, we pass the new value as an argument.
For example, the error-print-width parameter controls the maximum width of an error message:
1 2 3 | (error-print-width) ; 250 (default value) (error-print-width 500) ; sets parameter to 500 (error-print-width) ; now 500 |
As shown above, we can set parameters imperatively, by passing a value as an argument. But parameters also cooperate with parameterize, a let-like form that lets us temporarily set a parameter to a certain value during the evaluation of the parameterize body:
1 2 3 4 | (error-print-width) ; 250 (parameterize ([error-print-width 500]) (error-print-width)) ; 500 (error-print-width) ; 250 |
parameterize has two benefits. First, it remembers the original value of the parameter, and resets the parameter to that value after parameterize exits. Second, the effect of the new parameter value is bounded: it only affects expressions invoked, directly or indirectly, during the body of the parameterize.
Let’s see an example using displayln. We create ostr, an output port that captures output to a string. Then we use parameterize to temporarily use it as the current-output-port, and call func:
1 2 3 4 5 6 7 8 | (define (func str) (displayln str)) (define ostr (open-output-string)) (parameterize ([current-output-port ostr]) (func "inside")) ; output temporarily redirected to ostr (func "outside") (display (format "ostr = ~a" (get-output-string ostr))) |
1 2 | outside ostr = inside |
The func within the parameterize has no visible output, because the output of displayln is being redirected to ostr. In other words, parameterize affects displayln, even though it’s called from inside func. But after the parameterize, we can call func and it will display text normally. We can also retrieve the stored contents of ostr.
With that in mind, we’ll invert our approach. Rather than suppressing print statements only when the module is imported, we’ll suppress all print statements by default. When the module is run directly, we’ll use the configure-runtime submodule to make them visible. How?
We’ll create a new parameter called basic-output-port that holds an output port. By default, this port will discard any output.
In our main "expander.rkt" module, we’ll wrap the main program loop in parameterize, and set the current-output-port to be this new basic-output-port. Why? Because then, every time displayln is invoked by a print statement, the output will be sent to basic-output-port. And by default, it will be discarded.
Our configure-runtime submodule will call do-setup!, which will set the basic-output-port to the usual current-output-port. That way, when the module is run directly, the results of print statements will be visible.
That gives us enough guidance to finish our "setup.rkt" module:
1 2 3 4 5 6 7 8 | #lang br (provide basic-output-port do-setup!) (define basic-output-port (make-parameter (open-output-nowhere))) (define (do-setup!) (basic-output-port (current-output-port))) |
We define basic-output-port as a new parameter using make-parameter. To make an output port that discards its output, we use open-output-nowhere.
In do-setup!, we imperatively set basic-output-port to be the same as current-output-port.
The key updates are in "expander.rkt". At the top, we import our new "setup.rkt" module so we can use the basic-output-port parameter. We also extract the EXPORT-NAME syntax objects the same way we did in imports—by calling find-property, this time with the 'b-export-name key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #lang br/quicklang (require "struct.rkt" "run.rkt" "elements.rkt" "setup.rkt") (provide (rename-out [b-module-begin #%module-begin]) (all-from-out "elements.rkt")) (define-macro (b-module-begin (b-program LINE ...)) (with-pattern ([((b-line NUM STMT ...) ...) #'(LINE ...)] [(LINE-FUNC ...) (prefix-id "line-" #'(NUM ...))] [(VAR-ID ...) (find-property 'b-id #'(LINE ...))] [(IMPORT-NAME ...) (find-property 'b-import-name #'(LINE ...))] [(EXPORT-NAME ...) (find-property 'b-export-name #'(LINE ...))]) #'(#%module-begin (module configure-runtime br (require basic/setup) (do-setup!)) (require IMPORT-NAME ...) (provide EXPORT-NAME ...) (define VAR-ID 0) ... LINE ... (define line-table (apply hasheqv (append (list NUM LINE-FUNC) ...))) (parameterize ([current-output-port (basic-output-port)]) (void (run line-table)))))) (begin-for-syntax (require racket/list) (define (find-property which line-stxs) (remove-duplicates (for/list ([stx (in-list (stx-flatten line-stxs))] #:when (syntax-property stx which)) stx) #:key syntax->datum))) |
At the top of our #%module-begin syntax template, we add the configure-runtime submodule that we saw earlier. All it does is import basic/setup and call do-setup!. + We can’t use a relative path like "setup.rkt" inside the submodule. Why not? Because this submodule will be evaluated within a source file that lives elsewhere in the filesystem. So we need a full module path.
Below that, we also add a provide form that contains our new list of EXPORT-NAME ... items.
At the bottom of the #%module-begin syntax template, we wrap our main program loop in parameterize, setting the current-output-port to our new basic-output-port parameter.
Finally, as we did for b-import, we need to convert b-export nodes into (void) expressions. So we update "misc.rkt":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #lang br (require "struct.rkt" "expr.rkt") (provide b-rem b-print b-let b-input b-import b-export) (define (b-rem val) (void)) (define (b-print . vals) (displayln (string-append* (map ~a vals)))) (define-macro (b-let ID VAL) #'(set! ID VAL)) (define-macro (b-input ID) #'(b-let ID (let* ([str (read-line)] [num (string->number (string-trim str))]) (or num str)))) (define-macro (b-import NAME) #'(void)) (define-macro (b-export NAME) #'(void)) |
Now we can test our original "sample-exporter.rkt" module:
1 2 3 4 5 6 7 8 9 | #lang basic 10 def div(num, denom) = num / denom 20 x = 5 : y = 10 30 print x - 4 40 x = 15 : y = 30 50 print div(y, x) 60 x = 20 70 print div((x + x + x), x) 80 export div : export x |
If we did everything right, when we run this program directly, it will display its output normally:
1 2 3 | 1 2 3 |
What a relief. We should also be able to import the module. This time, we expect the 1 2 3 output will be suppressed, and we’ll only see the output from "sample-importer.rkt":
1 2 3 4 5 | #lang br (require basic/sample-exporter) div x (div x 10) |
1 2 3 | #<procedure:div> 20 2 |
Very nice. Let’s also check that trying to export an undefined identifier gives us the error we were hoping for:
1 2 | #lang basic 10 export x |
1 | module: provided identifier not defined or imported for phase 0 in: x |
Though we had to take a slightly scenic route, our exports work the way we hoped.
Fortunately, everything we learned about the configure-runtime will be useful in the next section, where we reprogram the REPL.