Here’s what we’ll add to our BASIC implementation in this tutorial:
In the last tutorial, we did some prep work for the syntax colorer. But now we’ll write it.
We’ll extend our use of exceptions to support better line errors.
It seems like all the trendy programming languages have them, so we’ll add variables and input.
We’ll move beyond addition to support a wider range of mathematical expressions, which will require us to consider some subtle issues about associativity and precedence of operations.
We’ll extend our expressions with conditionals, including the if statement.
And for the grand finale, we’ll add the trickiest BASIC commands, gosub and for loops, making short work of them through the magic of continuations.
We’ll start with the code we made during the first basic tutorial. We’ll also assume that we’ve created a basic directory and installed it as a package as described in the previous specification and setup.
But before we hit the road, let’s reorganize the project to be more modular.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #lang br (require brag/support) (define-lex-abbrev digits (:+ (char-set "0123456789"))) (define basic-lexer (lexer-srcloc ["\n" (token 'NEWLINE lexeme)] [whitespace (token lexeme #:skip? #t)] [(from/stop-before "rem" "\n") (token 'REM lexeme)] [(:or "print" "goto" "end" "+" ":" ";") (token lexeme 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) |
1 2 3 4 5 6 7 8 9 10 | #lang br (require "lexer.rkt" brag/support) (define (make-tokenizer ip [path #f]) (port-count-lines! ip) (lexer-file-path path) (define (next-token) (basic-lexer ip)) next-token) (provide make-tokenizer) |
1 2 3 4 5 6 7 8 9 10 11 12 13 | #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-statement : b-end | b-print | b-goto b-rem : REM b-end : /"end" b-print : /"print" [b-printable] (/";" [b-printable])* @b-printable : STRING | b-expr b-goto : /"goto" b-expr b-expr : b-sum b-sum : b-number (/"+" b-number)* @b-number : INTEGER | DECIMAL |
1 2 3 4 5 6 7 8 9 10 11 | #lang br/quicklang (require "parser.rkt" "tokenizer.rkt") (module+ reader (provide read-syntax)) (define (read-syntax path port) (define parse-tree (parse path (make-tokenizer port path))) (strip-bindings #`(module basic-mod basic/expander #,parse-tree))) |
We’ll trim down our expander module to contain only our #%module-begin macro. Our supporting structure types will be moved into a new "struct.rkt" module. Our run function will go into "run.rkt". Everything else—i.e., the functions and macros that implement the language constructs—will be moved into a new "elements.rkt" module. We use all-from-out to re-export everything that gets imported from this module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #lang br/quicklang (require "struct.rkt" "run.rkt" "elements.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 ...))]) #'(#%module-begin LINE ... (define line-table (apply hasheqv (append (list NUM LINE-FUNC) ...))) (void (run line-table))))) |
Here’s the new "struct.rkt" module. We use struct-out to automatically include the supporting functions that are silently generated when we define a new structure type with struct:
1 2 3 4 5 6 | #lang br (provide (struct-out end-program-signal) (struct-out change-line-signal)) (struct end-program-signal ()) (struct change-line-signal (val)) |
The new "run.rkt", which relies on both "struct.rkt" and "line.rkt" (described below):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #lang br (require "line.rkt" "struct.rkt") (provide run) (define (run line-table) (define line-vec (list->vector (sort (hash-keys line-table) <))) (with-handlers ([end-program-signal? (λ (exn-val) (void))]) (for/fold ([line-idx 0]) ([i (in-naturals)] #:break (>= line-idx (vector-length line-vec))) (define line-num (vector-ref line-vec line-idx)) (define line-func (hash-ref line-table line-num)) (with-handlers ([change-line-signal? (λ (cls) (define clsv (change-line-signal-val cls)) (or (and (exact-positive-integer? clsv) (vector-member clsv line-vec)) (error (format "error in line ~a: line ~a not found" line-num clsv))))]) (line-func) (add1 line-idx))))) |
Our new "elements.rkt" module will bring together a number of other supporting modules. Again, we use all-from-out to re-export the imported bindings, thus there are no definitions in the body of this module:
1 2 3 4 5 6 | #lang br (require "line.rkt" "go.rkt" "expr.rkt" "misc.rkt") (provide (all-from-out "line.rkt" "go.rkt" "expr.rkt" "misc.rkt")) |
Then we need to make the four modules that are imported by "elements.rkt".
First we have "line.rkt", which provides b-line:
1 2 3 4 5 6 7 8 9 | #lang br (require "struct.rkt") (provide b-line) (define-macro (b-line NUM STATEMENT ...) (with-pattern ([LINE-NUM (prefix-id "line-" #'NUM #:source #'NUM)]) (syntax/loc caller-stx (define (LINE-NUM) (void) STATEMENT ...)))) |
Then "go.rkt", which provides b-end and b-goto:
Then "expr.rkt", which provides b-sum and b-expr:
And finally "misc.rkt", which holds everything else, which for now is b-rem and b-print:
So all the code is the same. We’ve just divided it into smaller modules.
We can test that we’ve set up everything correctly by running our sample program from last time:
1 2 3 4 5 6 7 8 9 | #lang basic 30 rem print 'ignored' 35 50 print "never gets here" 40 end 60 print 'three' : print 1.0 + 3 70 goto 11. + 18.5 + .5 rem ignored 10 print "o" ; "n" ; "e" 20 print : goto 60.0 : end |
The result should be the same as before:
1 2 3 4 | one three 4 |
If so, we’re ready to move on.