Expr/c and wrap-expr/c not error highlighting what contract states it is blaming

Don't know if I'm doing something incorrect or if this is just a bug. Been having a nightmare of a time trying to get expr/c and wrap-expr/c to work with the macros I've been writing. Whenever I attempt to use one of them and click the singular red octagon with a cross in it, it almost always takes me into either the internal contract.rkt or kw.rkt file. The smallest breaking example I could come up with is this:

#lang racket
(require (for-syntax syntax/contract
                     syntax/parse))

(define-syntax (test stx) 
  (syntax-parse stx
    [(_ x)
     #:with x/c (wrap-expr/c #'integer? #'x)
     #'x/c]))

(test '(a b c))

Running that code on my system causes the error highlighting to occur in contract.rkt and returns this error message:

../../../../nix/store/rgfyzwxj5s8783kb1p4cw33fdpa5lpg3-racket-8.13/share/racket/collects/syntax/contract.rkt:95:21: test: contract violation
  expected: integer?
  given: '(a b c)
  in: integer?
      macro argument contract
  contract from: 
      /home/sora/Documents/research-notes/testing-macro-contracts.rkt
  blaming: /home/sora/Documents/research-notes/testing-macro-contracts.rkt
   (assuming the contract is correct)
  at: /home/sora/Documents/research-notes/testing-macro-contracts.rkt:13:6

Any idea what I may be doing wrong?

1 Like

My lazy armchair guess: You need to use syntax/loc instead of syntax a.k.a. #'.

So instead of #'x/c a.k.a. (syntax x/c): You could use (syntax/loc x x/c) to give the new, returned syntax the same srcloc as the original x piece of syntax (instead of the default srcloc inside the macro).


Although there are probably better explanations/examples, I wrote a blog post when I first learned about this. It also links to a post about the situation where you want the srcloc on a specific piece inside a begin.

1 Like

No dice using syntax/loc, the error highlighting still appears in contract.rkt unfortunately (Thank you for your blog posts and fear of macros btw, both have been invaluable for me)

Ah, sorry.

So it looks like the text at the end of the contract blame message knows the correct location:

  at: /home/sora/Documents/research-notes/testing-macro-contracts.rkt:13:6

The problem is that the runtime error location used by Dr Racket is not there -- instead it's inside the macro definition in contract.rkt.


When I change the end of your program from

(test '(a b c))

to

(with-handlers ([exn? (λ (e)
                        (println e)
                        (println (exn:srclocs? e)) ;; #f
                        (when (exn:fail:contract:blame? e)
                          (println (blame-source (exn:fail:contract:blame-object e)))))])
  (test '(a b c)))

It outputs:

(exn:fail:contract:blame
 "test: contract violation\n  expected: integer?\n  given: '(a b c)\n  in: integer?\n      macro argument contract\n  contract from: \n      /home/greg/src/racket/examples/wrap-expr.rkt\n  blaming: /home/greg/src/racket/examples/wrap-expr.rkt\n   (assuming the contract is correct)\n  at: /home/greg/src/racket/examples/wrap-expr.rkt:17:8"
 #<continuation-mark-set>
 #<blame-yes-swap>)
#f
(srcloc #<path:/home/greg/src/racket/examples/wrap-expr.rkt> 17 8 513 8)

What I'm (mis?)understanding from this:

The contract failure exception:

  • does not supply a useful location in the continuation mark set (you get inside the macro)
  • does not supply any exn:srclos (another possible provider of locations)
  • does supply a blame object, with the correct srcloc -- however DrRacket (apparently) and Racket Mode (definitely) don't look for this

So... I'm not sure if this is a deficiency in wrap-expr/c or contract blame exceptions, or, a to-do item for tools like DrR and Racket Mode (i.e. "here is yet another place to look for error locations"), or, some other issue I'm failing to understand. Maybe @robby can say more?

It looks like the goal is to get a better stacktrace for the error message, if I'm understanding correctly, so that would have to be something inside wrap-expr/c or things it calls (coupled with using errortrace which, iiuc, OP is already using), not the contract system or the IDE.

At this point I think I'll pass the @ to @ryanc :sweat_smile:

1 Like

IIUC: Having exn:fail:contract:blame support prop:exn:srclocs (and supply the same srcloc as blame-source) would suffice to make "go to error" to work for Racket Mode and Dr Racket.

[Otherwise, I could imagine tools adding this as another possible source of error locations. Which I'm willing to do, in Racket Mode. But that might justify adding a new doc page for tool authors, to inventory these sources. :smile:]

I tried that, and I see what you mean. The diff is straightforward, if we decide to go that way, namely:

index 4226e31d8f..1023b2917a 100644
--- a/racket/collects/racket/contract/private/blame.rkt
+++ b/racket/collects/racket/contract/private/blame.rkt
@@ -345,6 +345,8 @@
 (define (show-blame-negative b) (show-blame blame-negative b))
 
 (define-struct (exn:fail:contract:blame exn:fail:contract) [object]
+  #:property prop:exn:srclocs
+  (λ (x) (list (blame-source (exn:fail:contract:blame-object x))))
   #:transparent)

The only possible problem I see is that it means that the location where the contract gets attached is going to take priority over the location where the violation is discovered. In the example here, they are the same, but with functions they'll be different and contract-out is going to make it always be in the importing module. For example, with this these two files:

#lang racket
(provide
 (contract-out
  [f (-> integer? integer?)]))
(define (f x) x)
#lang racket
(require "tmp.rkt")
(f #f)

(where the first one is named "tmp.rkt") the error location is always in tmp.rkt even though the top of the stack is a very useful spot to look.

But maybe that's more of a problem with the way that DrRacket is interpreting the information it finds in the exception record?

I'm not sure what's the right thing here. What do you think?

Yeah, it does seem to be getting to be a lot, doesn't it! :smile: It definitely seems like we should be able to use an existing path or we should have a clearer justification for adding a new one and the various IDEs should all be updated to use it.

I agree that in your example, the call site is the preferred, most relevant location.

Two questions in response that occur to me:

  1. Today, the at: clause of the blame message describes a location -- but only textually. Exposing that as structured data -- as a srcloc -- seems like a reasonable thing to do.

    Under what conditions the at: blame location is meaningful, or most relevant -- that seems like an independent question?

  2. Today, already, there exists the situation where an exn supplies srclocs, but of course there also exists "context" (whether from errortrace or continuation marks).

    So already we have the question, when both are supplied, which to prefer, when.

    What Racket Mode does: If there are exn-srclocs use those and ignore the context locs. Dr Racket does similar, I think?

    But... is that right? I think that heuristic has worked OK, until now, but AFAIK it's not documented or justified anywhere.

    So if exposing contract blame in exn-srclocs and that heuristic aren't compatible, I'm not sure which of the two is "wrong"?

    I could see appending both lists, but that doesn't answer knowing which to give precedence.

I'm not sure this is written down anywhere, but I'd always though of the source location that's in a syntax error as the source location to show in an IDE and the prop:exn:srclocs as an extensible way to allow other exceptions to offer such source locations, as effectively, commands to the IDE to send the focus to that spot. That's why prop:exn:srclocs supersedes the stacktrace locations, at least in drr.

I'm open to rethinking this or generalizing it somehow.

Separately from that question, there is also the question of what source location we shoudl be showing here. I feel like the source location where the contract is established is an interesting one, if someone wants to find out what the contract actually was. But it seems secondary to the spot where the error was discovered (the top of the stack). That's what led to my suggestion that we should change wrap-expr/c so that the top of the stack comes out right in examples like at the top of this thread.

I think actually I'm OK with the status quo.

Note that things like rackunit test failures also use prop:exn:srclocs, IIRC. Not just "syntax errors" per se. The status quo has worked fine for those cases, too.

That's why I immediately jumped to the idea of exn:fail:contract:blame using prop:exn:srclocs. But if instead you can "change the top of the stack" for both errortrace and continuation marks, that also works!

Yep, the part where I suggested, oh just use the blame object location, was simplistic.

That would only work if someone does the work to change the blame object location to the desired, user-apparent top of the stack.

And maybe also change the "at:" location text, so they all agree?

Ah, right! And that one also fits the "please show this location" as people probably want to see the precise test case that fails when one does. So this seems to be working.

I'm not following this part, in particular I'm not sure how we're going to get the top of the stack to put into the record.

I could also change the word "at" to be something more suggestive like "contract established at" or something?

I think the top example here cued me to misremember what the blame object a.k.a. "at:" location is supposed to be (and even though you'd explained it above). Sorry!

IIUC now, it's not meant to be the location blamed for the violation. So N/A for "the most important error location to show".

Instead it's extra info about about the contract itself.

Error messages are hard, but yes, I think that would help.


So I guess the TL;DR is -- of course -- to do your original idea:

In case anyone else is dealing with a similar problem and is looking for a workaround, catching and re-raising a new exception type that supports prop:exn:srclocs using the srcloc of the blame like greg mentioned at least appears to fix most of my problems:

#lang racket
(require (for-syntax syntax/contract
                     syntax/parse))

(define a-custom-contract
  (listof (listof (or/c 'z 'y 'x))))

(define-struct (exn:fail:contract:blame0 exn:fail:contract) [object]
  #:property prop:exn:srclocs
  (lambda (x) (list (blame-source (exn:fail:contract:blame0-object x))))
  #:transparent)

(define-syntax (test stx) 
  (syntax-parse stx
    [(_ x)
     #:with x/c (wrap-expr/c #'a-custom-contract #'x)
     #'(with-handlers ([exn:fail:contract:blame?
                        (lambda (e) (raise (exn:fail:contract:blame0 (exn-message e)
                                                                     (current-continuation-marks)
                                                                     (exn:fail:contract:blame-object e))))])
         x/c)]))

(test '(a b c))

Hi @sora22 : I'm glad you found something that works for you! One caution: if you're using contracts for functions, this may not give the best results. If you don't know if the contract is going to be like that or not, then you can test at runtime and either swap the exception or not, depending on if the contract answers true to flat-contract?.