Before we add more features to the language, let’s improve our error handling, using what we learned about errors and exceptions in the first basic tutorial.
Every BASIC program consists of numbered lines. So every run-time error arises from a particular line in the program. We’d like to make it easy to report errors in terms of a specific line:
1 2 3 | error in line 20: line 6000 not found error in line 60: next without for error in line 70: return without gosub |
The functions that implement the statements in a line are responsible for raising a given error (e.g., "next without for"). The wrinkle is that they don’t know anything about what line they’re being called from (e.g., "in line 60"). So the challenge is bringing these two parts together to make one error message.
Where can we get the line number? Our b-line macro has access to NUM, a syntax-pattern variable that holds the line number:
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 ...)))) |
One possible approach: we could pass NUM as an extra argument to each STATEMENT. But that would be a drag, because we’d have to change the interface of every function that implements a statement to take a line-number argument. Also, because a run-time error can be raised anywhere, we’d have to keep passing around this line-number value within the program just in case it’s needed. In sum: bad idea.
We could also set a global variable with the current line number, and then all the statement-implementing functions could read it. Save that for the next edition of JavaScript: The Good Parts.
Here’s a better idea: we’ll construct our line error in two steps, using exceptions to transmit the pieces:
The function that first raises the error will call a new raise-line-error helper that raises an exception of type line-error, with the specific error message as input:
1 | (raise-line-error "return without gosub") |
This line-error exception will be caught by the line function at the top of the calling chain using an exception handler. This exception handler will prepend "error in line X" to the original message. Then it will raise a new error using this new message. That way, we’ll end up with errors in the style we want:
1 | error in line 70: return without gosub |
That should work pretty well. But let’s also handle one small complication.
Even though every run-time error can be associated with a line in the program, the error may not be discovered until control has left the line function.
One example is goto. Suppose we have a goto in our program that points at a nonexistent line:
1 | 70 goto 1001001 |
We get the following error:
1 | error in line 70: line 1001001 not found |
But this error isn’t detected inside the line function. Rather, it’s only detected when run handles the change-line-signal exception from goto and discovers that the line number doesn’t exist. At that point, run raises an error, written in the friendly style:
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))))) |
But this is a duplication of effort. Rather than handle a line-specific error outside the line function, it would be better policy to centralize the handling of every line error inside the related line function.
We’ll be using exceptions to handle errors that arise “from below”. But we can handle errors that arise “from above” by adding an optional #:error argument to every line function. Rather than creating its own error, run can pass an error message back to the line function, which can do the rest. + Perhaps obviously, we can’t use an exception to communicate the error message from run, because the line function has already exited.
First we add a new line-error structure type to our "struct.rkt". line-error has one field, msg, that will store the specific error message:
1 2 3 4 5 6 7 8 | #lang br (provide (struct-out end-program-signal) (struct-out change-line-signal) (struct-out line-error)) (struct end-program-signal ()) (struct change-line-signal (val)) (struct line-error (msg)) |
By the way, in Racket projects, structure types are often stored in their own module so they can be shared easily between other modules in the project.
Then we need to update "line.rkt":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #lang br (require "struct.rkt") (provide b-line raise-line-error) (define-macro (b-line NUM STATEMENT ...) (with-pattern ([LINE-NUM (prefix-id "line-" #'NUM #:source #'NUM)]) (syntax/loc caller-stx (define (LINE-NUM) (with-handlers ([line-error? (λ (le) (handle-line-error NUM le))]) (void) STATEMENT ...))))) (define (raise-line-error error-msg) (raise (line-error error-msg))) (define (handle-line-error num le) (error (format "error in line ~a: ~a" num (line-error-msg le)))) |
We add a raise-line-error function that will take an error-msg argument. This is what other functions will use to launch a line-error exception. Within the b-line macro, we wrap the body of the line function in with-handlers, which will detect a line-error? exception and pass it to handle-line-error, along with the current line number. In turn, handle-line-error will make the full error message and pass it to error, which will raise a standard error of type exn:fail.
That takes care of errors arising from below. Now let’s handle the ones coming from above, by adding an optional #:error argument to our line-function definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #lang br (require "struct.rkt") (provide b-line raise-line-error) (define-macro (b-line NUM STATEMENT ...) (with-pattern ([LINE-NUM (prefix-id "line-" #'NUM #:source #'NUM)]) (syntax/loc caller-stx (define (LINE-NUM #:error [msg #f]) (with-handlers ([line-error? (λ (le) (handle-line-error NUM le))]) (when msg (raise-line-error msg)) STATEMENT ...))))) (define (raise-line-error error-msg) (raise (line-error error-msg))) (define (handle-line-error num le) (error (format "error in line ~a: ~a" num (line-error-msg le)))) |
By default, the msg argument is #f. So when the line function is called normally, we won’t get an error. But we replace the (void) with a when that will use raise-line-error to create an error when msg exists. + We can delete the (void) because when evaluates to (void) if its condition is false.
Finally, we return to "run.rkt" and rephrase the line-not-found error using the new #:error argument:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #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)) (line-func #:error (format "line ~a not found" clsv))))]) (line-func) (add1 line-idx))))) |
We’ll test raise-line-error when we get into our BASIC statements. For now, we can check that goto errors work the way we expect:
1 2 | #lang basic 70 goto 1001001 |
1 | error in line 70: line 1001001 not found |
True, this is exactly the same as the error we got before. But it shows that our new control flow is working correctly. The "line X not found" message is being passed from run to line-func, which adds the line number and reports the error.
1 2 3 4 5 6 7 8 | #lang br (provide (struct-out end-program-signal) (struct-out change-line-signal) (struct-out line-error)) (struct end-program-signal ()) (struct change-line-signal (val)) (struct line-error (msg)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #lang br (require "struct.rkt") (provide b-line raise-line-error) (define-macro (b-line NUM STATEMENT ...) (with-pattern ([LINE-NUM (prefix-id "line-" #'NUM #:source #'NUM)]) (syntax/loc caller-stx (define (LINE-NUM #:error [msg #f]) (with-handlers ([line-error? (λ (le) (handle-line-error NUM le))]) (when msg (raise-line-error msg)) STATEMENT ...))))) (define (raise-line-error error-msg) (raise (line-error error-msg))) (define (handle-line-error num le) (error (format "error in line ~a: ~a" num (line-error-msg le)))) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #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)) (line-func #:error (format "line ~a not found" clsv))))]) (line-func) (add1 line-idx))))) |