`~describe` vs. `#:role` in `syntax-parse`

How should we choose between ~describe descriptions and #:role for error information with syntax-parse?

Consider this macro, inspired by a discussion on Discord:

#lang racket

(require (for-syntax syntax/parse))

(define-syntax hash-refs
  (syntax-parser
    [(_ (~var hsh expr #:role "hash")
        (~describe "key with optional default"
                   (~seq (~var key expr #:role "key")
                         (~optional (~seq #:or (~describe "default expression"
                                                          default:expr)))))
        ...)
     #`(let ([h hsh])
         (values (hash-ref h key (~? default)) ...))]))

The incorrect use (hash-refs #hash() 'foo #:or) gives a helpful error message:

hash-refs: expected more terms starting with default expression
  parsing context: 
   while parsing key with optional default

If we replace the (~describe "default expression" default:expr) form with (~var default expr #:role "default"), the error message is less useful and does not mention the role:

hash-refs: expected more terms starting with expression
  parsing context: 
   while parsing key with optional default

For completeness, I also tried (~describe #:role "default" "Expression" default:expr), which produced (again without mentioning the role):

hash-refs: expected more terms starting with Expression
  parsing context: 
   while parsing key with optional default

Maybe this is just a bug, and these error messages should mention the role?

I think ~describe makes the most sense when it's divorced from the context of a particular macro, especially since using it is equivalent to moving the pattern it's wrapping into a syntax class and giving that syntax class a #:description.

The #:role option makes more sense (to me) for disambiguation between two pieces of input syntax that are supposed to have the same shape, but which are used for different purposes. It seems similar to the distinction between types and parameter names to me.

With these ideas in mind, I rewrote your macro above as follows:

#lang racket

(require syntax/parse/define)


(begin-for-syntax
  (define-splicing-syntax-class hash-refs-clause
    #:attributes (key default)
    #:description "key with optional default"
    (pattern (~seq key (~optional (~seq #:or (~describe "default expression" default:expr))))
      #:declare key expr #:role "key")))

(define-syntax-parse-rule (hash-refs hsh clause:hash-refs-clause ...)
  #:declare hsh expr #:role "hash"
  (let ([h hsh])
    (values (hash-ref h clause.key (~? clause.default)) ...)))

As best as I can tell, this produces the same error messages. Note also that I decided to use #:declare with #:role instead of using ~var, since in my opinion the use of ~var makes it more difficult to understand the structure of the macro's input.

But I'm not sure what to do about that (~describe "default expression" default:expr) bit. That's an interesting case.

Separately though, we can use syntax classes to get a slight improvement on the error message. In the error message for the incorrect use (hash-refs #hash() 'foo #:or), the context is given as while parsing key with optional default. But once we've seen #:or, that default doesn't seem so optional to me anymore. If we restructure the syntax classes like so:

(begin-for-syntax
  (define-splicing-syntax-class hash-refs-clause
    #:attributes (key default)
    #:description #false
    (pattern key
      #:declare key expr #:role "key"
      #:attr default #false)
    (pattern :key-with-default))

  (define-splicing-syntax-class key-with-default
    #:attributes (key default)
    #:description "key with default"
    (pattern (~seq key #:or (~describe "default expression" default:expr))
      #:declare key expr #:role "key")))

(define-syntax-parse-rule (hash-refs hsh clause:hash-refs-clause ...)
  #:declare hsh expr #:role "hash"
  (let ([h hsh])
    (values (hash-ref h clause.key (~? clause.default)) ...)))

...then the error message for (hash-refs #hash() 'foo #:or) will correctly notify us that the default expression is not optional in that context, since we've supplied #:or:

hash-refs: expected more terms starting with default expression
  parsing context: 
   while parsing key with default in: (hash-refs #hash() (quote foo) #:or)