Error in Error

Discussion in 'AutoCAD' started by Ken Alexander, Aug 6, 2004.

  1. Ken Alexander

    David Bethel Guest

    (setq olderr *error*
    *error* (lambda (e)
    (while (> (getvar "CMDACTIVE") 0)
    (command))
    (and (/= e "quit / exit abort")
    (princ (strcat "\nError: *** " e " *** ")))
    (and (= (logand (getvar "UNDOCTL") 8) 8)
    (command "_.UNDO" "_END" "_.U")))


    (*error* "Try This")
    (princ "\nNow Watch Me Keep Running")

    *error* itself does not terminate a program when called directly. The
    program only stop when ACAD has encountered an error. It then calls
    *error* upon it's own exit/abort. You would have to include an (exit)
    command in the *error* statement in order to stop the processing
    mechanism. -David
     
    David Bethel, Aug 7, 2004
    #21
  2. Ken Alexander

    David Bethel Guest

    I posted the thread (120 KB) in the customer file group. -David
     
    David Bethel, Aug 7, 2004
    #22
  3. Ken Alexander

    Owen Wengerd Guest

    Jason:
    I haven't read the entire thread, so maybe this has been discussed
    already. It's not exactly the question you asked, but consider that when
    you define your own local *error* function, you hide the previous *error*
    function. Once it is hidden, there is no way to call it.

    This brings up one point I believe I argued in the CompuServe thread: your
    error handler should be nice and call the previous error handler (if there
    was one) so that the calling function can perform any necessary cleanup.
    The only way you can call the previous error handler is if you save it
    before you hide it by defining a local *error* function. :)
     
    Owen Wengerd, Aug 8, 2004
    #23
  4. Ken Alexander

    Tom Smith Guest

    Oops, the useful test stringp is imaginary. How about (= (type msg) 'STR) ?
     
    Tom Smith, Aug 8, 2004
    #24
  5. Ken Alexander

    Tom Smith Guest

    Okay, here's a version of my approach that manages to use defun-q-list-set (although setq would work just as well):

    Library functions loaded at startup:

    (defun silent-undo (option / oce)
    (setq oce (getvar "cmdecho"))
    (setvar "cmdecho" 0)
    (command "undo" option)
    (setvar "cmdecho" oce))

    (defun show-error-msg (msg)
    (or
    (/= (type msg) 'STR)
    (member msg '("Function cancelled" "quit / exit abort"))
    (princ (strcat "\nError: " msg)))
    (if *debug*
    (vl-bt)))

    (defun restore-vars (varlist)
    (mapcar
    '(lambda (var)
    (list 'setvar var (getvar var)))
    varlist))

    (defun *init-error* (varlist)
    (silent-undo "begin")
    (defun-q-list-set '*error*
    (append
    '((msg))
    '((show-error-msg msg))
    (restore-vars varlist)
    '((silent-undo "end"))
    '((princ))))
    ;(print(defun-q-list-ref '*error*))
    (defun-q-list-set '*no-error*
    (append
    '(nil)
    (restore-vars varlist)
    '((silent-undo "end"))
    '((princ))))
    ;(print(defun-q-list-ref '*no-error*))
    (princ))

    *init-error* takes a list of variables to be saved, and defines two cleanup functions at runtime: *error* which will be triggered by an error condition, and *no-error* which allows me to do the same thing without being acused of creating an error. Uncomment the two lines to see the function definitions.

    Note that the only difference is that *no-error* doesn't provide for echoing a message, which AFAIK is the only thing that the built-in *error* does.

    Usage:

    (defun c:myfunction (/ *error*)
    (*init-error* '("cmdecho" "osmode"))
    ...
    (do-whatever)
    ...
    (*no-error*) ;no error here!
    )

    Of course (*error* nil) would accomplish the same thing as (*no-error*)..
     
    Tom Smith, Aug 8, 2004
    #25
  6. Owen,

    I debate need to do this in all cases (granted, it may make sense in some
    cases). After all, other languages, such as VB(A) don't give you that sort
    of ability. If I define an error handler for a procedure, I expect that
    error handler to deal with only the errors possible _while in that
    procedure_. The calling procedure will deal with errors in whatever fashion
    it needs to, not expecting a subr to pass errors back up the line.

    For instance, in VB(A) I often use a subr to get an object. That subr
    handlers errors when the object cannot be returned, and returns Nothing to
    the calling function. The calling function is coded to deal with the object
    being Nothing and does not expect an error from the subr.

    But then, I'm preaching to a guru now... ;^)

    --
    R. Robert Bell


    Jason:
    I haven't read the entire thread, so maybe this has been discussed
    already. It's not exactly the question you asked, but consider that when
    you define your own local *error* function, you hide the previous *error*
    function. Once it is hidden, there is no way to call it.

    This brings up one point I believe I argued in the CompuServe thread: your
    error handler should be nice and call the previous error handler (if there
    was one) so that the calling function can perform any necessary cleanup.
    The only way you can call the previous error handler is if you save it
    before you hide it by defining a local *error* function. :)
     
    R. Robert Bell, Aug 8, 2004
    #26
  7. Of course, in my hasty post I neglected to consider that error handling in
    lisp is not the same as VB(A), of course.

    I suppose that if I had a subr that required an error handler of its own, I
    would certainly call the application-level error handler also. I guess I had
    blinders on just because of the *error* symbolin your post.

    My bad!
     
    R. Robert Bell, Aug 8, 2004
    #27
  8. Really? What gave you that idea?
     
    Tony Tanzillo, Aug 8, 2004
    #28
  9. Ken Alexander

    Owen Wengerd Guest

    (RRB this is a reply to you, but it's really directed at all the
    participants)

    The problem is that in Lisp, when you exit *error*, all lisp evaluation
    ceases. When you define your own *error* that doesn't call the previous
    error handler, no calling code ever gets a chance to run its own error
    handler.

    I think what you are not considering is that every subr you write may be
    called by some other subr (not necessarily one that you wrote). Your subr
    should give the calling subr an opportunity to clean up if an error occurs.

    Consider the following scenario. You write an application for exporting
    some drawing data to an external file. You expose a function named (C:DOIT)
    that prompts the user for a filename and writes data to that file. As the
    user of your application, I decide I prefer to type the filename instead of
    selecting it from a dialog box. So I write a function called (C:-DOIT) that
    sets FILEDIA to zero, calls (C:DOIT), then restores FILEDIA. In the event
    of an error in (C:DOIT), I want (C:-DOIT)'s error handler to be called so I
    can restore FILEDIA.

    Using the typical error handling code, (C:DOIT) does it's own cleanup then
    exits the lisp interpreter without giving (C:-DOIT) a chance to do any
    cleanup. (C:DOIT) *should* call (C:_DOIT)'s error handler in it's own error
    handler. That means the previous *error* should be saved before (C:DOIT)
    hides it (and that means you can't declare *error* as a local in C:DOIT).

    I think my argument is best described by example. Write a function
    (C:DOIT) using your standard error handling mechanism, then consider the
    scenario where I as the end user (or as another developer) write the
    following "wrapper" function:

    (defun C:-DOIT (/ C:-DOIT::instance_data C:-DOIT::initialize
    C:-DOIT::cleanup C:-DOIT::error C:-DOIT::main)
    (defun C:-DOIT::error (msg)
    (C:-DOIT::cleanup C:-DOIT::instance_data)
    (if (member (type *error*) '(SUBR USUBR))
    (*error* msg)
    (progn (if msg (princ (strcat "\n" msg))) (princ))
    )
    )
    (defun C:-DOIT::initialize (/ data)
    (setq data (list *error* (getvar "FILEDIA")))
    (setq *error* C:-DOIT::error)
    (setvar "FILEDIA" 0)
    data
    )
    (defun C:-DOIT::cleanup (data)
    (setq *error* (car data))
    (setvar "FILEDIA" (cadr data))
    )
    (setq C:-DOIT::instance_data (C:-DOIT::initialize))
    (C:DOIT)
    (C:-DOIT::cleanup C:-DOIT::instance_data)
    (princ)
    )

    Now, what happens when the user cancels (C:DOIT) by pressing <Esc>? Does
    FILEDIA get restored to it's previous value? I think not. But if you wrote
    (C:DOIT) like the following, it would:

    (defun C:DOIT (/ C:DOIT::instance_data C:DOIT::initialize C:DOIT::cleanup
    C:DOIT::error C:DOIT::main)
    (defun C:DOIT::error (msg)
    (C:DOIT::cleanup C:DOIT::instance_data)
    (if (member (type *error*) '(SUBR USUBR))
    (*error* msg)
    (progn (if msg (princ (strcat "\n" msg))) (princ))
    )
    )
    (defun C:DOIT::initialize (/ data)
    (setq data (list *error* (getvar "CMDECHO")))
    (setq *error* C:DOIT::error)
    (setvar "CMDECHO" 0)
    data
    )
    (defun C:DOIT::cleanup (data)
    (setq *error* (car data))
    (setvar "CMDECHO" (cadr data))
    )
    (defun C:DOIT::main ()
    (initget "Yes No")
    (if (/= "No" (getkword "Throw an error? [<Yes>/No]: "))
    (exit)
    )
    )
    (setq C:DOIT::instance_data (C:DOIT::initialize))
    (C:DOIT::main)
    (C:DOIT::cleanup C:DOIT::instance_data)
    (princ)
    )

    This simple example demonstrates why it's important to call the previous
    error handler if there was one. If there was a previous error handler, then
    your code should let *it* decide how to signal the error to the user. This
    allows consumers of your function to "superclass" it without modifying
    *your* code. So you see I'm not talking about local subfunctions, but main
    functions that may be called from someone else's code.

    I hope that helps clarify my position. :)
     
    Owen Wengerd, Aug 8, 2004
    #29
  10. Right, but in any rational programming system, why should
    one have to explicitly call an enclosing error handler to start
    with?

    The basic architectural flaw of AutoLISP and Visual LISP, is
    that they lack any concept of termination handling (try/finally)
    and worse, have no funtional equivalent to 'throw':

    While it's certainly possible for nested error handlers to
    chain to each other *explicitly*, by pushing/popping them, I'd
    hardly call that 'sound' programming :), mainly because it
    requires all of the code to make unreasonable assumptions,
    and regardless of anything, there is no way for any error
    handler implemented via *error* to 'unhandle' an error and
    allow normal execution to resume. vl-catch-all-apply does
    allow that, but suffers from the lack of an equivalent to 'throw'
    for re-raising a handled error, and hence, offers no rational way
    to do termination handling (try/finally).

    void SomeFunction()
    {
    try
    {
    SomeOtherFunction();
    }
    catch (...)
    {
    // Cleanup code here always runs
    throw;
    }
    }

    void SomeOtherFunction();
    {
    try
    {
    SomethingDangerous();
    }
    catch (...)
    {
    // Cleanup code here always runs
    throw;
    }
    }
     
    Tony Tanzillo, Aug 8, 2004
    #30
  11. Ken Alexander

    Owen Wengerd Guest

    Tony:
    One shouldn't, but given that AutoLISP doesn't provide any kind of stack
    unwinding, I think chaining error handlers is safe and reasonable. I think
    calling the previous *error* function is a workable alternative to
    rethrowing (but I liken it more to stack unwinding except the chained code
    runs in the context of the nested function instead of it's enclosing
    function). Also I think calling (exit) from code is a reasonable
    alternative to throw. :)
     
    Owen Wengerd, Aug 8, 2004
    #31
  12. Owen -
    AutoLISP does provide stack unwinding, but the problem is not that,
    it is the nature of the way error handling is implemented (via a user-
    defined function assigned to the symbol *error*).

    As far as chaining error handlers manually goes, I don't agree. Not
    only do I feel it is unreasonable, it's actually quite dangerous. You
    yourself hinted at the reason why:
    If an error handler references variables that are locally bound to its
    associated function, and that error handler is then executed in the
    context or local environment of another called function instead, the
    called function can also have locals assigned to the same symbols
    bound to the caller, and may be referenced by the caller's error
    handler, which means that those symbols will not have the values
    they had in the caller, and the error handler will see the wrong values.

    (defun outer ( / foo olderr)
    (setq olderr *error*)

    (defun *error* (msg)
    (princ (strcat "\nValue of 'foo' in outer *error* handler: " foo))
    )

    (setq foo "begin outer")
    (inner)
    (setq *error* olderr)
    )

    (defun inner ( / foo olderr)
    (setq olderr *error*)

    (defun *error* (msg)
    (if olderr (olderr msg)) ;; chain to previous *error* handler
    )

    (setq foo "begin inner")

    ;; simulate a hard error
    (exit)

    (setq *error* olderr)
    )

    (defun C:TEST ()
    (outer)
    )

    Load and run this and look at what the *error* handler for the
    outer function sees as the value of the symbol 'FOO, which is
    local to both the outer and inner functions.

    Regarding the use of (exit), it doesn't work because it causes
    Visual LISP to stop everything and report that an 'error has
    occurred inside the *error* function'. It does work when using
    (vl-catch-all-apply), but it doesn't allow the error condition to
    be conveyed to any but the initial error handler, so I don't see
    it as a reasonable workaround.

    IMO, If a programming language requires this much hacking and
    kludgery, to achieve reasonable behavior that is commonplace
    in most other languages, the argument in favor of going to these
    extremes begins to look more like a defense of using the language
    in the first place, and why one should continue using it, in spite
    of all its inherent flaws.
     
    Tony Tanzillo, Aug 9, 2004
    #32
  13. Do you happen to know whether AutoDesk has ever inspected what it would
    take to replace AutoLisp/VLisp with a full-size Common Lisp? That would
    correct most of AutoLisp's flaws, and building a compatibility package
    to run on top of it to allow old AutoLisp programs to run would be
    reasonably simple.

    --
     
    Martti Halminen, Aug 9, 2004
    #33
  14. There's been talk of it publicly by customers, and they
    may have kicked it around, but they're clearly moving
    away from LISP and towards things like .NET. I doubt
    very much that there'll be any major change in course.

    http://www.caddzone.com

    AcadXTabs: MDI Document Tabs for AutoCAD 2004/2005
    http://www.acadxtabs.com
     
    Tony Tanzillo, Aug 9, 2004
    #34
  15. I should have also pointed out that IMO, the best that
    LISP enthusiasts can hope for at this point, might be a
    LISP implementation that compiles to MSIL.

    A ray of hope (perhaps): http://dotlisp.sourceforge.net/dotlisp.htm
     
    Tony Tanzillo, Aug 9, 2004
    #35
  16. Ken Alexander

    Doug Broad Guest

    Tony,
    Excellent points. Gven the limitations of LISP, have
    you found any techniques that minimize the drawbacks
    without hacking or kludgery?

    Tony and all,
    What do you use your error handlers for?

    I generally use them to:
    1) Restore system variables to their previous state.
    2) Close file handles.
    3) Perhaps undo a partially drawn object that was
    cancelled by the user.
    4) Release certain ActiveX objects.
    5) Print the error message

    Where possible I try to use methods that do not
    affect the system variables. This reduces the need for
    the error handlers.

    I try to avoid nesting the error handlers, preferring where
    possible, to allow the top level error handler clean up
    the related subfunctions.

    <untested conjecture (kludgery?)>
    Each application's top level function could maintain
    a list in a symbol local to the top level function that
    subfunctions could use to perform cleanup and undo operations.
    Each subfunction could run initialization(push) and cleanup
    (pop) operations on the same stack global to the top
    level. The *error* handler could process all the statements
    remaining in the cleanup list stack at the time of an actual
    error or user cancellation.
    Initialization routines could pass values of the local
    symbol names directly to the global stack, thus eliminating
    the problem with local variables hidden from the top level.
    Separate stacks for normal cleanup and undo cleanup might
    be helpful.
    Rather than redefining the *error* routine at each level, a
    single error routine at the top level would process the stack
    for all the subroutines.

    Regards,
    Doug
     
    Doug Broad, Aug 9, 2004
    #36
  17. Doug - I've tried many things over the years. None of them
    to my complete satisfaction. The solution for me, was to
    not attempt to use LISP for work that required that degree of
    sophistication in error handlng.
     
    Tony Tanzillo, Aug 9, 2004
    #37
  18. Ken Alexander

    David Bethel Guest

    Doug,

    My guess is that other than during development, 90+ % of the time, all
    the error trapping needs to do is to clean up after someone has canceled
    the routine. The routine shouldn't bomb on it's own. And even if ( exit
    ) is used, just clean up the mess you made, reset everything to the way
    it was before the routine was called, and be on your way.

    Yes, you can try and get fancy and have the error trapping do this or do
    that, but I find leads to more problems than it is worth. ADESK gave up
    on AutoLisp a while ago, and never fully finished the deal or provided a
    robust error trapping mechanism. My $0.02 -David
     
    David Bethel, Aug 9, 2004
    #38
  19. Tom,

    I wasn't saying that *error* is flawed as a function. I was simply
    pointing out something that I had noticed happening quite frequently.
    Just got me curious as to why this hasn't been pointed out. In fact I
    found it again in reading a very detailed thread by on error handling.
    I found exactly what I was trying to accomplish in my error handler in
    that thread. But again, there was that small error in the error
    handler. I then found that a custom error function would recurse on
    itself one and only one time. I was just curious and felt like
    pointing it out. At the same time, if *error* expects a string as an
    arg then why pass a "bad" arg then test for it and make the necessary
    conditions when you can simply pass an empty string. That was the
    original idea behind this thread.

    With that aside, I have been working on an error handler that I can
    use as my "toolbox" error handler that would be easy to use in all
    routines. Our other discussions on error handlers as a list of
    expressions got me going on this. I saw where I can keep my error
    handler small and compact but complete. Then I found a post by Bobby
    Jones on error handling. This was exactly what I was looking to do.
    The defun-q-list's aside (I know setq works just as well) here is my
    new error handler. A spin off of Bobby's.

    Usage:

    (defun c:test (/ *int-error* *error* ent ent2)
    (*int-error* '("osmode" "cmdecho" "clayer"))
    (setvar "cmdecho" 0)
    (setq ent (car (entsel)))
    (redraw ent 3)
    (add-cmd (list 'redraw 'ent 4))
    (setvar "osmode" 0)
    (setq ent2 (entsel))
    (redraw ent 4)
    (del-cmd (list 'redraw 'ent 4))
    (command "line" pause pause "")
    (entsel)
    (clean-exit)
    )



    (defun *int-error* (varlist)
    (vl-load-com)

    ;;;Defines var-set function list.
    (defun-q var-set ()
    (command "._Undo" "_e")
    (command "._U")
    (princ)
    )

    ;;;Adds expression lists of variables to be reset.
    (defun-q-list-set
    'var-set
    (append '((/))
    '((command "._Undo" "_e"))
    '((command "._U"))
    (mapcar
    (function (lambda (var) (list 'setvar var (getvar var))))
    varlist
    )
    (cdddr (defun-q-list-ref 'var-set))
    )
    )

    ;;;Allows adding command expressions to var-set.
    (defun add-cmd (cmd)
    (if (not (vl-position cmd var-set))
    (defun-q-list-set
    'var-set
    (append '((/))
    '((command "._Undo" "_e"))
    '((command "._U"))
    (list cmd)
    (cdddr (defun-q-list-ref 'var-set))
    )
    )
    )
    )

    ;;;Removes command expressions from var-set.
    (defun del-cmd (cmd)
    (defun-q-list-set 'var-set (vl-remove cmd var-set))
    )

    ;;;Exits clean, quiet, & resets vars. Removes undo from var-set.
    (defun clean-exit ()
    (del-cmd (list 'command "._U"))
    (var-set)
    )

    (add-cmd (list 'setvar "cmdecho" (getvar "cmdecho")))
    (setvar "cmdecho" 0)
    (command "._Undo" "_be")

    ;;;Redefine standard *error*. Make sure *error* is declared
    ;;;as a local variable in the main routine.
    (defun *error* (msg)
    (setvar "cmdecho" 0)
    (if (not (vl-position msg
    '("console break"
    "Function cancelled"
    "quit / exit abort")
    )
    )
    (princ (strcat "\nError: " msg))
    )
    (while (= 1 (logand (getvar "cmdactive") 1))
    (command)
    )
    (var-set)
    )
    )

    --
    Ken Alexander
    Acad2004
    Windows XP

    "We can't solve problems by using the same kind
    of thinking we used when we created them."
    --Albert Einstein

    defun-q-list-set (although setq would work just as well):
    cleanup functions at runtime: *error* which will be triggered by an
    error condition, and *no-error* which allows me to do the same thing
    without being acused of creating an error. Uncomment the two lines to
    see the function definitions.
    echoing a message, which AFAIK is the only thing that the built-in
    *error* does.
    (*no-error*)..
     
    Ken Alexander, Aug 9, 2004
    #39
  20. Ken Alexander

    Owen Wengerd Guest

    Doug:
    In addition, your code should handle the situation where someone else
    writes code that calls your code (then your error handler is no longer the
    top level handler, and your handler should call the new top level error
    handler). :)
     
    Owen Wengerd, Aug 9, 2004
    #40
Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.