1
Fork 0
mirror of https://git.savannah.gnu.org/git/guile.git synced 2025-05-20 03:30:27 +02:00

Document stack-overflow handlers, limits, and unwind-only exceptions

* module/system/repl/error-handling.scm (call-with-error-handling): Add
  #:report-keys kwarg, so that unwind-only exceptions (stack-overflow in
  particular) get reported.

* doc/ref/api-debug.texi (Pre-Unwind Debugging): Add documentation for
  #:report-keys kwarg of call-with-error-handling.
  (Stack Overflow): New subsubsection.
  (Debug Options): Remove discussion of stack overflow.
This commit is contained in:
Andy Wingo 2014-03-16 15:27:26 +01:00
parent f57d4316c2
commit 22806c244a
2 changed files with 204 additions and 82 deletions

View file

@ -1,6 +1,6 @@
@c -*-texinfo-*-
@c This is part of the GNU Guile Reference Manual.
@c Copyright (C) 1996, 1997, 2000, 2001, 2002, 2003, 2004, 2007, 2010, 2011, 2012, 2013
@c Copyright (C) 1996, 1997, 2000, 2001, 2002, 2003, 2004, 2007, 2010, 2011, 2012, 2013, 2014
@c Free Software Foundation, Inc.
@c See the file guile.texi for copying conditions.
@ -341,6 +341,7 @@ library, or from Guile itself.
* Catching Exceptions:: Handling errors after the stack is unwound.
* Capturing Stacks:: Capturing the stack at the time of error.
* Pre-Unwind Debugging:: Debugging before the exception is thrown.
* Stack Overflow:: Detecting and handling runaway recursion.
* Debug Options:: A historical interface to debugging.
@end menu
@ -599,10 +600,12 @@ These procedures are available for use by user programs, in the
@deffn {Scheme Procedure} call-with-error-handling thunk @
[#:on-error on-error='debug] [#:post-error post-error='catch] @
[#:pass-keys pass-keys='(quit)] [#:trap-handler trap-handler='debug]
[#:pass-keys pass-keys='(quit)] @
[#:report-keys report-keys='(stack-overflow)] @
[#:trap-handler trap-handler='debug]
Call a thunk in a context in which errors are handled.
There are four keyword arguments:
There are five keyword arguments:
@table @var
@item on-error
@ -629,9 +632,185 @@ traps entirely. @xref{Traps}, for more information.
@item pass-keys
A set of keys to ignore, as a list.
@item report-keys
A set of keys to always report even if the post-error handler is
@code{catch}, as a list.
@end table
@end deffn
@node Stack Overflow
@subsubsection Stack Overflow
@cindex overflow, stack
@cindex stack overflow
Every time a Scheme program makes a call that is not in tail position,
it pushes a new frame onto the stack. Returning a value from a function
pops the top frame off the stack. Stack frames take up memory, and as
nobody has an infinite amount of memory, deep recursion could cause
Guile to run out of memory. Running out of stack memory is called
@dfn{stack overflow}.
@subsubheading Stack Limits
Most languages have a terrible stack overflow story. For example, in C,
if you use too much stack, your program will exhibit ``undefined
behavior'', which if you are lucky means that it will crash. It's
especially bad in C, as you neither know ahead of time how much stack
your functions use, nor the stack limit imposed by the user's system,
and the stack limit is often quite small relative to the total memory
size.
Managed languages like Python have a better error story, as they are
defined to raise an exception on stack overflow -- but like C, Python
and most dynamic languages still have a fixed stack size limit that is
usually much smaller than the heap.
Arbitrary stack limits would have an unfortunate effect on Guile
programs. For example, the following implementation of the inner loop
of @code{map} is clean and elegant:
@example
(define (map f l)
(if (pair? l)
(cons (f (car l))
(map f (cdr l)))
'()))
@end example
However, if there were a stack limit, that would limit the size of lists
that can be processed with this @code{map}. Eventually, you would have
to rewrite it to use iteration with an accumulator:
@example
(define (map f l)
(let lp ((l l) (out '()))
(if (pair? l)
(lp (cdr l) (cons (f (car l)) out))
(reverse out))))
@end example
This second version is sadly not as clear, and it also allocates more
heap memory (once to build the list in reverse, and then again to
reverse the list). You would be tempted to use the destructive
@code{reverse!} to save memory and time, but then your code would not be
continuation-safe -- if @var{f} returned again after the map had
finished, it would see an @var{out} list that had already been
reversed. The recursive @code{map} has none of these problems.
Guile has no stack limit for Scheme code. When a thread makes its first
Guile call, a small stack is allocated -- just one page of memory.
Whenever that memory limit would be reached, Guile arranges to grow the
stack by a factor of two. When garbage collection happens, Guile
arranges to return the unused part of the stack to the operating system,
but without causing the stack to shrink. In this way, the stack can
grow to consume up to all memory available to the Guile process, and
when the recursive computation eventually finishes, that stack memory is
returned to the system.
@subsubheading Exceptional Situations
Of course, it's still possible to run out of stack memory. The most
common cause of this is program bugs that cause unbounded recursion, as
in:
@example
(define (faulty-map f l)
(if (pair? l)
(cons (f (car l)) (faulty-map f l))
'()))
@end example
Did you spot the bug? The recursive call to @code{faulty-map} recursed
on @var{l}, not @code{(cdr @var{l})}. Running this program would cause
Guile to use up all memory in your system, and eventually Guile would
fail to grow the stack. At that point you have a problem: Guile needs
to raise an exception to unwind the stack and return memory to the
system, but the user might have throw handlers in place (@pxref{Throw
Handlers}) that want to run before the stack is unwound, and we don't
have any stack in which to run them.
Therefore in this case, Guile throws an unwind-only exception that does
not run pre-unwind handlers. Because this is such an odd case, Guile
prints out a message on the console, in case the user was expecting to
be able to get a backtrace from any pre-unwind handler.
@subsubheading Runaway Recursion
Still, this failure mode is not so nice. If you are running an
environment in which you are interactively building a program while it
is running, such as at a REPL, you might want to impose an artificial
stack limit on the part of your program that you are building to detect
accidental runaway recursion. For that purpose, there is
@code{call-with-stack-overflow-handler}, from @code{(system vm vm)}.
@example
(use-module (system vm vm))
@end example
@deffn {Scheme Procedure} call-with-stack-overflow-handler limit thunk handler
Call @var{thunk} in an environment in which the stack limit has been
reduced to @var{limit} additional words. If the limit is reached,
@var{handler} (a thunk) will be invoked in the dynamic environment of
the error. For the extent of the call to @var{handler}, the stack limit
and handler are restored to the values that were in place when
@code{call-with-stack-overflow-handler} was called.
Usually, @var{handler} should raise an exception or abort to an outer
prompt. However if @var{handler} does return, it should return a number
of additional words of stack space to allow to the inner environment.
@end deffn
A stack overflow handler may only ever ``credit'' the inner thunk with
stack space that was available when the handler was instated. When
Guile first starts, there is no stack limit in place, so the outer
handler may allow the inner thunk an arbitrary amount of space, but any
nested stack overflow handler will not be able to consume more than its
limit.
Unlike the unwind-only exception that is thrown if Guile is unable to
grow its stack, any exception thrown by a stack overflow handler might
invoke pre-unwind handlers. Indeed, the stack overflow handler is
itself a pre-unwind handler of sorts. If the code imposing the stack
limit wants to protect itself against malicious pre-unwind handlers from
the inner thunk, it should abort to a prompt of its own making instead
of throwing an exception that might be caught by the inner thunk.
@subsubheading C Stack Usage
It is also possible for Guile to run out of space on the C stack. If
you call a primitive procedure which then calls a Scheme procedure in a
loop, you will consume C stack space. Guile tries to detect excessive
consumption of C stack space, throwing an error when you have hit 80% of
the process' available stack (as allocated by the operating system), or
160 kilowords in the absence of a strict limit.
For example, looping through @code{call-with-vm}, a primitive that calls
a thunk, gives us the following:
@lisp
scheme@@(guile-user)> (use-modules (system vm vm))
scheme@@(guile-user)> (let lp () (call-with-vm lp))
ERROR: Stack overflow
@end lisp
Unfortunately, that's all the information we get. Overrunning the C
stack will throw an unwind-only exception, because it's not safe to
do very much when you are close to the C stack limit.
If you get an error like this, you can either try rewriting your code to
use less stack space, or increase the maximum stack size. To increase
the maximum stack size, use @code{debug-set!}, for example:
@lisp
(debug-set! stack 200000)
@end lisp
The next section describes @code{debug-set!} more thoroughly. Of course
the best thing is to have your code operate without so much resource
consumption by avoiding loops through C trampolines.
@node Debug Options
@subsubsection Debug options
@ -679,59 +858,6 @@ to historical oddities, it is a macro that expects an unquoted option
name.
@end deffn
@subsubheading Stack overflow
@cindex overflow, stack
@cindex stack overflow
Stack overflow errors are caused by a computation trying to use more
stack space than has been enabled by the @code{stack} option. There are
actually two kinds of stack that can overflow, the C stack and the
Scheme stack.
Scheme stack overflows can occur if Scheme procedures recurse too far
deeply. An example would be the following recursive loop:
@lisp
scheme@@(guile-user)> (let lp () (+ 1 (lp)))
<unnamed port>:8:17: In procedure vm-run:
<unnamed port>:8:17: VM: Stack overflow
@end lisp
The default stack size should allow for about 10000 frames or so, so one
usually doesn't hit this level of recursion. Unfortunately there is no
way currently to make a VM with a bigger stack. If you are in this
unfortunate situation, please file a bug, and in the meantime, rewrite
your code to be tail-recursive (@pxref{Tail Calls}).
The other limit you might hit would be C stack overflows. If you call a
primitive procedure which then calls a Scheme procedure in a loop, you
will consume C stack space. Guile tries to detect excessive consumption
of C stack space, throwing an error when you have hit 80% of the
process' available stack (as allocated by the operating system), or 160
kilowords in the absence of a strict limit.
For example, looping through @code{call-with-vm}, a primitive that calls
a thunk, gives us the following:
@lisp
scheme@@(guile-user)> (use-modules (system vm vm))
scheme@@(guile-user)> (debug-set! stack 10000)
scheme@@(guile-user)> (let lp () (call-with-vm lp))
ERROR: In procedure call-with-vm:
ERROR: Stack overflow
@end lisp
If you get an error like this, you can either try rewriting your code to
use less stack space, or increase the maximum stack size. To increase
the maximum stack size, use @code{debug-set!}, for example:
@lisp
(debug-set! stack 200000)
@end lisp
But of course it's better to have your code operate without so much
resource consumption, avoiding loops through C trampolines.
@node Traps
@subsection Traps

View file

@ -1,6 +1,6 @@
;;; Error handling in the REPL
;; Copyright (C) 2001, 2009, 2010, 2011, 2012, 2013 Free Software Foundation, Inc.
;; Copyright (C) 2001, 2009, 2010, 2011, 2012, 2013, 2014 Free Software Foundation, Inc.
;; This library is free software; you can redistribute it and/or
;; modify it under the terms of the GNU Lesser General Public
@ -42,7 +42,8 @@
(define* (call-with-error-handling thunk #:key
(on-error 'debug) (post-error 'catch)
(pass-keys '(quit)) (trap-handler 'debug))
(pass-keys '(quit)) (trap-handler 'debug)
(report-keys '(stack-overflow)))
(let ((in (current-input-port))
(out (current-output-port))
(err (current-error-port)))
@ -92,6 +93,14 @@
((disabled) #f)
(else (error "Unknown trap-handler strategy" trap-handler))))
(define (report-error key args)
(with-saved-ports
(lambda ()
(run-hook before-error-hook)
(print-exception err #f key args)
(run-hook after-error-hook)
(force-output err))))
(catch #t
(lambda ()
(with-default-trap-handler le-trap-handler
@ -103,17 +112,15 @@
(if (memq key pass-keys)
(apply throw key args)
(begin
(with-saved-ports
(lambda ()
(run-hook before-error-hook)
(print-exception err #f key args)
(run-hook after-error-hook)
(force-output err)))
(report-error key args)
(if #f #f)))))
((catch)
(lambda (key . args)
(if (memq key pass-keys)
(apply throw key args))))
(when (memq key pass-keys)
(apply throw key args))
(when (memq key report-keys)
(report-error key args))
(if #f #f)))
(else
(if (procedure? post-error)
(lambda (k . args)
@ -147,15 +154,9 @@
((@ (system repl repl) start-repl) #:debug debug)))))))
((report)
(lambda (key . args)
(if (not (memq key pass-keys))
(begin
(with-saved-ports
(lambda ()
(run-hook before-error-hook)
(print-exception err #f key args)
(run-hook after-error-hook)
(force-output err)))
(if #f #f)))))
(unless (memq key pass-keys)
(report-error key args))
(if #f #f)))
((backtrace)
(lambda (key . args)
(if (not (memq key pass-keys))
@ -165,13 +166,8 @@
(make-stack #t)
;; Narrow as above, for the debugging case.
3 tag 0 (and tag 1))))
(with-saved-ports
(lambda ()
(print-frames frames)
(run-hook before-error-hook)
(print-exception err #f key args)
(run-hook after-error-hook)
(force-output err)))
(with-saved-ports (lambda () (print-frames frames)))
(report-error key args)
(if #f #f)))))
((pass)
(lambda (key . args)