[plt-scheme] Source location infromation and tracebacks (was: match-lambda and source location)

From: Matthew Flatt (mflatt at cs.utah.edu)
Date: Sun Jan 4 19:34:49 EST 2009

At Sun, 04 Jan 2009 19:16:20 +0100, Jakub Piotr Cłapa wrote:
> On 1/3/09 8:11 PM, Matthew Flatt wrote:
> > At Sat, 03 Jan 2009 00:54:53 +0100, Jakub Piotr Cłapa wrote:
> >> match-lambda creates a lambda without overriding syntax information.
> >> This greatly reduces the information content in stack traces.
> >>
> >> OTOH overriding the source information for the lambda keyword greatly
> >> reduces the clarity of the macro code...
> >
> > I don't understand what you mean. Can you provide an example, describe
> > how it behaves, and describe how it should behave instead?
> Ok. I think I misinterpreted the problem since I last stumbled upon it. 
> Here is the test case:
> (define test-1
>    (match-lambda
>      [(list a b) #t]))
> (define (test-2 l)
>    (match l
>      [(list a b) #t]))
> Notice the difference in tracebacks between (test-1 #t) and (test-2 #t).
> This is as far as I (now) understand related to source location 
> information of the match form.

Ah, I see what you mean...

> The problem with readability of the macros was in my own code:
> (define-syntax (on-notif stx)
>    (syntax-case stx ()
>      [(on-notif (notif arg ...)
>         expr expr+ ...)
>       (with-syntax ([fun (datum->syntax #'here
>                                         (syntax-e #'(lambda (arg ...)
>                                                       expr expr+ ...))
>                                         #'on-notif)])
>         #'(begin
>             #;(** 'notif (list fun (notif-observers notif)))
>             (notif-add-observer! notif fun)))]))

The `syntax/loc' form makes this more readable:

      (with-syntax ([fun (syntax/loc #'on-notif
                           #'(lambda (arg ...) expr expr+ ...))])
            (notif-add-observer! notif fun)))

As another example, the `match-lambda' form could be implemented as

     (define-syntax (match-lambda stx)
       (syntax-case stx ()
         [(k . clauses) (quasisyntax/loc stx 
                           (lambda (exp) 
                             #,(syntax/loc stx (match exp . clauses))))]))

This would cause the stack trace for `(test-1 #t)' to give more
information: it would show the `match-lambda' as a representative of
the implicit `match' form. While that would give more information, it
would look strange, because evaluating a `match-lambda' form doesn't
actually evaluate its body. In other words, the `match-lambda' form
isn't really a good representative for the macro-introduced `match'.

> > I may misunderstand this part, too, but I think the first half is what
> > Errortrace (in the "errortrace" collection) does. It instruments code
> > to push continuation marks that indicates source locations, and then
> > the exception handler collects the marks to reconstruct a stack trace.
> > Errortrace doesn't try to track tail calls beyond the immediate one.
> 1. My (uneducated) idea was to add a note every time the source location 
> info suggests that we changed from macro generated code to user code and 
> vice versa.
> 2. Tracing several previous tail calls could ease the debugging of loops 
> and should be possible without breaking the space efficiency of tail calls.
> This would provide for Scheme loops what tracebacks provide for normal 
> function calls.

I see what you mean about tail calls.

In your original example, though, tail calls are not quite the problem.
The real problem seems to be that there's no source expression within
`test-1' whose dynamic extent includes the exception-raising expression
--- that is, no expression in `test-1' to be a representative for the
macro-introduced `match' form.

In `test-2', the `match' expression's dynamic extent includes the
evaluation of the exception-raising expression. It turns out that the
exception-raising expression is called in tail position with respect to
the `match' form, but the exception-raising expression doesn't have a
source location. That's why the `match' form shows up in the stack
trace. That is, a certain amount of tail-call history is currently
preserved --- but only in that "no information" doesn't replace "some

Even more detail:

Why does `(test-2 #t)' show up in the stack trace, when the `match'
form (and therefore the exception-raising expression) replaces the
`(test-2 #t)' call? Shouldn't it get overwritten with the location of
the `match' form? Actually, it does, and the `(test-2 #t)' that you see
reported in the stack trace is actually the representative of something
like `(print-value (test-2 #t))', where the `print-value' part was
introduced by the module-body expander of the `scheme' language. If you
switch the module to the `mzscheme' language --- which doesn't inject
printing code --- you'll see that `(test-2 #t)' no longer appears in
the trace.

This also explains why `(test-1 #t)' shows up in the trace twice. The
deeper call is really the printing expression, while the inner one is
the function call --- and since there was no representative source
expression for the `match' within `test-1' (or the exception-raising
expression within the `match'), the location of the function call is
never replaced later.

Posted on the users mailing list.