
Toms Lisp Exception Handling
Tom's lisp supports a simple exception mechanism.
Exceptions can be generated by the system or the user, and
there are several ways to catch them.
Exceptions
Each exception
has a unique integer code number; there is no hierarchy of
exceptions as in Java or C++.
The the following exceptions are predefined:
Code |
|
Symbolic Name |
|
Description |
1 |
|
ERR_UNDEF |
|
Undefined identifier. This is thrown when you evaluate
an identifier which has no value. |
2 |
|
ERR_BADOP |
|
Inappropriate type in an operation. Presently, the only
way to get this one is to perform apply on something which is
not a function, usually because you typed
a list like (1 2 3) and forgot to
quote it, so you were trying to apply on
the integer 1. Many things which sound like
the name of this one actually belong to
ERR_BADTYPE. |
3 |
|
ERR_BADARG |
|
The argument list was badly-formed, for instance
(+ 2 3 . 5), where the argument list is not
a proper list. |
4 |
|
ERR_SHORTARG |
|
A function was called with too few arguments. |
5 |
|
ERR_BADTYPE |
|
A built-in function was called with the wrong argument
type, such as taking the car of a non-list.
Only built-in calls can generate this;
lambda functions and macros can be called with any
argument types. Of course, if they are called with
types they cannot handle, their primitives may
throw ERR_BADTYPE. |
6 |
|
ERR_LONGARG |
|
A function was called with too many arguments. |
7 |
|
ERR_RPAREN |
|
Syntax error: Right paren expected. |
8 |
|
ERR_SYN |
|
General syntax error. |
9 |
|
ERR_OPFAIL |
|
File open failed. The load operation can throw
this one. |
10 |
|
ERR_DIVZERO |
|
Division by zero. |
11 |
|
ERR_INTER |
|
Operation interrupted. This is thrown when the
process gets the SIGINT or SIGTERM
signals. These are sent by the keyboard ^C
or the kill command on Unix systems. |
12 |
|
ERR_MISC |
|
The miscellaneous error. This is an extra for use by
applications, and is not thrown by the
Tom's Lisp system. |
13 |
|
ERR_ABORT |
|
This is thrown by the abort primitive. It is
not thrown by the Tom's Lisp system. |
Error Objects
An exception is thrown when an operation returns an error object.
An exception object contains a code number from the above list, and
a string which describes the error. When an error occurs, the value
of the errant expression is an error object.
lsp>(/ 5 0)
**** Error 10: Division by zero. ****
(/ 5 0)
Here, we are performing the division operation on 5 and 0. The result is
not a number, but an error object containing the code 10 and the appropriate
division by zero message. In a very real sense, the system is acting just
as it does for a successful division, evaluating the expression and
printing the result. The result just isn't a number.
Also,
Tom's Lisp prints lots of nice asterisks when it
prints an error object.
Error objects are special in more ways than print format, however.
Any evaluation which sees an error
object stops whatever it was doing and returns the error.
Therefore, once an error is created,
all pending operations return, passing the error object up to the
top level.
lsp>(cons (+ 1 (/ 5 0)) '(5 9 1 4))
**** Error 10: Division by zero. ****
(cons (+ 1 (/ 5 0)) (quote (5 9 1 4)))
(+ 1 (/ 5 0))
(/ 5 0)
Here, the division returned an error object. The addition saw an
error object as its second parameter, so it returned it. The cons
operation saw the error object as its first parameter, so it returned
that. The value of cons was the error object, which
Tom's Lisp printed. A summary of the canceled operations is
printed with the error object.
If the system errors are not enough for you, you can create your own.
The error primitive takes an integer and a string and returns
the error object containing those. There is also an abort
primitive which is just a shorthand for error.
lsp>(error 98 "Sorry, Out Of Order")
**** Error 98: Sorry, Out Of Order ****
(error 98 "Sorry, Out Of Order")
lsp>(list 1 4 9 8 7 (error 34 "Hi there!") 23 9)
**** Error 34: Hi there! ****
(list 1 4 9 8 7 (error 34 "Hi there!") 23 9)
(error 34 "Hi there!")
lsp>(cons 15 (list 3 9 1 (abort) 2))
**** Error 13: Aborted ****
(cons 15 (list 3 9 1 (abort) 2))
(list 3 9 1 (abort) 2)
(abort)
(error 13 "Aborted")
Rather than choosing your own exception numbers, you should allocate them
with the errcode operation. It simply uses a counter to assign
the next number for your favorite error message name.
lsp>(errcode ERR_SMELLY)
14
Note thatlsp>(+ 17 (error ERR_SMELLY "Took shoes off"))
**** Error 14: Took shoes off ****
(+ 17 (error ERR_SMELLY "Took shoes off"))
(error 14 "Took shoes off")
Catching Errors
The rule that any operation which sees an error stops and
returns it is violated by a small number of operations which can
catch exceptions.
The simplest of these are succeeds and fails.
These take any number of expressions and
evaluate them left to right until all are done or one throws
an exception. They then return the appropriate
boolean, where “success” means completing without throwing an exception.
lsp>(succeeds (/ 8 9) (/ 3 4) (/ 2 1) (/ 10 4))
#t
lsp>(succeeds (/ 8 9) (/ 3 4) (/ 2 0) (/ 10 4))
nil
lsp>(fails (car 17))
#t
A more flexible and familiar form is try. It looks like this:
(try expr (code1 expr1) . . .
(coden exprn) [ (#t exprd) ] )
The form evaluates expr. If it throws no exception,
its value is the value of the try. If an exception is
thrown, the codes are compared, in order, to each
codei value, 1 through n. If one matches, the
corresponding expri is run, and its value is
returned. If none matches, and an exprd is present,
it is run and its value returned. Otherwise, the original
exception is re-thrown.
(define (oopsie x y)
(try (/ (car x) y)
(ERR_BADTYPE "Needs a list")
(ERR_DIVZERO 1)
)
)
lsp>(oopsie '(4 9 8) 2)
2
lsp>(oopsie '(5 9 1 2) 0)
1
lsp>(oopsie 4 9)
"Needs a list"
lsp>(oopsie '(2 1 9 8) (car 5))
**** Error 5: car: Bad arg type. ****
(oopsie (quote (2 1 9 8)) (car 5))
(car 5)
Note that in the final case, the error is not caught
by the try because it occurs when evaluating the parameters
before oopsie's body or its try are started.
It's also worth pointing out that try evaluates a single
expression, not a list of expression as do succeeds and
fails. You can use begin if you need to do more.
Generally, each codei will be just a single number,
which is compared to the exception number. However, if the
codei is a list, it is evaluated and treated as a boolean
to decide if its expri should be returned.
While being evaluated, the
symbols ERROR and MESSAGE are bound to the code and
message part, respectively, of the exception. So:
lsp>(try (/ 4 0) (ERR_DIVZERO 99))
99
lsp>(try (/ 4 0) ((= ERROR ERR_DIVZERO) 99))
99
Here, we've done the same thing twice, the easy way and the hard
way.
If any expri's throw an exception, it cannot be caught by a later
codej in the same try. That exception will be thrown by
the try construct.
The fundamental operation for catching
exceptions is called, unsurprisingly, catch.
It is a bit unwieldy, and is used mainly as the primitive
on which the above operations are based. Normal applications
do not generally use it.
That catch operation takes any number of
expressions, and evaluates them from left to right.
If one of these throws an exception,
catch returns the pair of nil and the
pair of the error code and the message from the error object.
If not,
catch returns the pair of #t and the value of the
last expression evaluated.
lsp>(catch (/ 8 9) (/ 3 4) (/ 2 1) (/ 10 4))
(#t . 2)
lsp>(catch (/ 8 9) (/ 3 4) (/ 2 0) (/ 10 4))
(nil 10 . "Division by zero.")
lsp>(cdr (catch (/ 8 9) (/ 3 4) (/ 2 1) (/ 10 4)))
2
lsp>(cdr (catch (/ 8 9) (/ 3 4) (/ 2 0) (/ 10 4)))
(10 . "Division by zero.")
Once an expression throws an exception, no more
are evaluated.