summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndy Wingo <wingo@pobox.com>2014-03-16 15:27:26 +0100
committerAndy Wingo <wingo@pobox.com>2014-03-16 15:32:39 +0100
commit22806c244a624d3fd801ba739ca0670702815a6e (patch)
treec3aaef385a43d789dcb236b700d6c512066960a2
parentf57d4316c2048f0c58a47c56b63d25d10511f98f (diff)
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.
-rw-r--r--doc/ref/api-debug.texi238
-rw-r--r--module/system/repl/error-handling.scm48
2 files changed, 204 insertions, 82 deletions
diff --git a/doc/ref/api-debug.texi b/doc/ref/api-debug.texi
index 32f32caba..cdd7a0960 100644
--- a/doc/ref/api-debug.texi
+++ b/doc/ref/api-debug.texi
@@ -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
diff --git a/module/system/repl/error-handling.scm b/module/system/repl/error-handling.scm
index d0d7967a3..eea7b9701 100644
--- a/module/system/repl/error-handling.scm
+++ b/module/system/repl/error-handling.scm
@@ -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)