Embedded ECL Lisp error handling fetch default error string and possibly line number

576 views Asked by At

Please see #7755661 first. I am using ECL and basically want to execute some code, trap any kind of condition that may occur and then continue execution, without prompting or entering the debugger. This is easy to achieve with the following handler-case macro:

(handler-case
  (load "code.lisp") ; this may raise a condition

  (error (condition)
    (print condition))) ; this prints sth like #<a UNBOUND-VARIABLE>

My only problem is that I cannot find a generic way to print a more meaningful error for the user. Indeed my application is an HTTP server and the output goes to a web page. code.lisp is written by the user and it can raise any kind of condition, I do now want to list them all in my code. I would just like to print the same error message I see on the REPL when I do not use handler-case, but in the HTML page, e.g. for an "unbound variable" error, a string like "The variable VAR is unbound".

By inspecting a condition object of type UNBOUND-VARIABLE I see it has two slots: SI:REPORT-FUNCTION, which is a compiled function and SI:NAME, set to the name of the variable in this case. I guess SI:REPORT-FUNCTION could be what I need to invoke but how can I call it? If I try:

(handler-case foo (error (condition) (SI::REPORT-FUNCTION condition)))

it tells me that SI:REPORT-FUNCTION is undefined. SI or SYS in ECL is a package for functions and variables internal to the implementation, but I don't worry if my code is not portable, as long as it works.

BTW in other kinds of condition objects there are also other apparently useful slots for my purpose, named SI:FORMAT-CONTROL and SI:FORMAT-ARGUMENT, but I cannot access any of them from my code too.

I was looking for somethink alike to the getMessage() method of Java exception objects in Lisp, but none of my sources ever mentions something like that.

Moreover, is there any hope to be able to get the line number in code.lisp where the error occurred too? Without that it would be difficult for the user to locate the problem in his code.lisp source file. I would really want to provide this information and stopping at the first error is acceptable for me.

2

There are 2 answers

2
Rainer Joswig On BEST ANSWER

In Common Lisp when print escaping is disabled, the error message is printed.

 CL-USER > (handler-case
               a       
             (error (condition)
               (write condition :escape nil)))

The variable A is unbound.
#<UNBOUND-VARIABLE 4020059743>

Note that PRINT binds *print-escape* to T.

Using PRINC works - it binds *print-escape* to NIL.

CL-USER > (handler-case
              a                
            (error (condition)
              (princ condition)))

The variable A is unbound.
#<UNBOUND-VARIABLE 4020175C0B>

This is described in CLHS 9.1.3 Printing Conditions.

Also note, when you have an object, which has a slot and the value of this slot is a function, then you need to get the slot value using the function SLOT-VALUE and then use FUNCALL or APPLY and call the function with the correct arguments.

If you have a condition of type simple-condition then it has a format-control and a format-argument information. This is described with an example how to use it for FORMAT in CLHS Function SIMPLE-CONDITION-FORMAT-CONTROL, SIMPLE-CONDITION-FORMAT-ARGUMENTS

0
Juanjo On

My answer below is based on one I already gave at the ECL mailing list. Actually I would claim that this is not an embedding problem, but a Lisp one. You want to get some information at the file position of the form which caused the error. This is not attached to a condition because conditions happen independently of whether the form evaluated was interpreted, compiled or part of a function that is already installed in the Lisp image. In other words, it is up to you to know the position of the file which is being read and do some wrapping that adds the information.

The following is nonstandard and prone to change: ECL helps you by defining a variable ext::source-location when LOAD is used on a source file. This variable contains a CONS that should NEVER be changed or stored by the user, but you can get the file as (CAR EXT:*SOURCE-LOCATION*) and the file position as (CDR EXT:*SOURCE-LOCATION*). The plan is then to embed your LOAD form inside a HANDLER-BIND

(defparameter *error-message* nil)
(defparameter *error-tag* (cons))

(defun capture-error (condition)
   (setf *error*
      (format nil "At character ~S in file ~S an error was found:~%~A"
         (cdr ext:*source-location*)
         (car ext:*source-location*)
         condition)))
  (throw *error-tag* *error-message*))

(defun safely-load (file)
  (handler-bind ((serious-condition #'capture-error))
      (catch *error-tag*
        (load file)
        nil)))

(SAFELY-LOAD "myfile.lisp") will return either NIL or the formatted error.

In any case I strongly believe that relying on LOAD for this is doomed to fail. You should create your own version of LOAD, starting from this

(defun my-load (userfile)
  (with-open-file (stream userfile :direction :input :external-format ....whateverformat...)
     (loop for form = (read stream nil nil nil)
        while form
        do (eval-form-with-error-catching form))))

where EVAL-FORM-.... implements something like the code above. This function can be made more sophisticated and you may keep track of file positions, line numbers, etc. Your code will also be more portable this way.

So please, read the ANSI Spec and learn the language. The fact that you did not know how to print readably a condition and instead tried to play with ECL internals shows that you might face further problems in the future, trying to go with non-portable solutions (hidden slot names, report functions, etc) instead of first trying the standard way.