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

**URL:** <https://racket.discourse.group/t/describe-vs-role-in-syntax-parse/3795>\
**Category:** General\
**Tags:** question, macro\
**Created:** [June 16, 2025, 8:18pm UTC](https://racket.discourse.group/t/describe-vs-role-in-syntax-parse/3795 "2025-06-16T20:18:22Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![LiberalArtist](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/liberalartist/32/151_2.png) [@LiberalArtist](https://racket.discourse.group/u/LiberalArtist)\
**Post date:** [June 16, 2025, 8:18pm UTC](https://racket.discourse.group/t/describe-vs-role-in-syntax-parse/3795/1 "2025-06-16T20:18:22Z")

</div>

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

Consider this macro, inspired by [a discussion on Discord](https://discord.com/channels/571040468092321801/1018246159208288366/1384253404464943105):

```scheme
#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:

```scheme
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:

```scheme
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):

```scheme
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?

---

<div class="post-metadata">

**Author:** ![notjack](https://avatars.discourse-cdn.com/v4/letter/n/e47774/32.png) [@notjack](https://racket.discourse.group/u/notjack)\
**Post date:** [June 17, 2025, 1:30am UTC](https://racket.discourse.group/t/describe-vs-role-in-syntax-parse/3795/2 "2025-06-17T01:30:56Z")

</div>

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:

```scheme
#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:

```scheme
(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`:

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

```

---

<div class="post-metadata">

**Author:** ![ryanc](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/ryanc/32/71_2.png) [@ryanc](https://racket.discourse.group/u/ryanc)\
**Post date:** [June 18, 2025, 5:50pm UTC](https://racket.discourse.group/t/describe-vs-role-in-syntax-parse/3795/3 "2025-06-18T17:50:38Z")

</div>

The "expected more terms starting with" error is special, since there is no term to "run" the syntax class on. The "starting with" part is only included if the description is a literal string, and it ignores the role. It would probably be possible to include the role if the role is also a literal string, though.

---

<div class="post-metadata">

**Author:** ![LiberalArtist](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/liberalartist/32/151_2.png) [@LiberalArtist](https://racket.discourse.group/u/LiberalArtist)\
**Post date:** [June 20, 2025, 11:10pm UTC](https://racket.discourse.group/t/describe-vs-role-in-syntax-parse/3795/4 "2025-06-20T23:10:14Z")

</div>

> [@notjack](#):
>
> using [`~default`] is equivalent to moving the pattern it's wrapping into a syntax class and giving that syntax class a `#:description`

Using a syntax class also by default has `~delimit-cut` behavior, and it changes the visibility of attributes.

> [@notjack](#):
>
> 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.

I like that!

> [@notjack](#):
>
> 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:
> 
> …
> 
> ...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`:
> 
> ```scheme
> hash-refs: expected more terms starting with default expression
> parsing context: 
> while parsing key with default in: (hash-refs #hash() (quote foo) #:or)
> 
> ```

Amusingly, my first implementation of this macro looked more like this version, including using a splicing syntax class with two `pattern`s.

However, for the erroneous use `(hash-refs test-hash 'foo #:and)`, your version here and mine at the top of this thread produce the same, reasonably nice error message:

```scheme
hash-refs: expected expression for key or expected the literal #:or
  parsing context: 
   while parsing different things... in: #:and

```

In contrast, this early version I wrote:

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

```

produces this worse error message, which just says "expression" instead of "required key expression":

```scheme
hash-refs: expected expression or expected the literal #:or
  parsing context: 
   while parsing different things... in: #:and

```

I was surprised to find that your implementation _didn't_ produce this message, because the structure of the patterns seems basically the same, other than inlining the syntax classes. Maybe there's some combination of `#:opaque`, `#:role`, or `~delimit-cut`/`~commit`/`~!` that would produce the same result, but I haven't found it yet.

Overall, I feel like I know how to use these features well enough to get reasonably good error messages (especially because `syntax-parse` does so much right automatically), but I don't feel like I have very strong intuitions about what changes to make when there's something I want to fine-tune: I work a lot out through trial-and-error experimentation.

Maybe I want some kind of debugger or inspector for `syntax-parse` error messages?

> [@ryanc](#):
>
> The "expected more terms starting with" error is special, since there is no term to "run" the syntax class on. The "starting with" part is only included if the description is a literal string, and it ignores the role. It would probably be possible to include the role if the role is also a literal string, though.

It hadn't even occurred to me that the role and description can be expressions, not just literal strings! That makes a lot of sense. And yes, including the role when possible does seem like a nice enhancement.

---

<div class="post-metadata">

**Author:** ![ryanc](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/ryanc/32/71_2.png) [@ryanc](https://racket.discourse.group/u/ryanc)\
**Post date:** [June 21, 2025, 2:45am UTC](https://racket.discourse.group/t/describe-vs-role-in-syntax-parse/3795/5 "2025-06-21T02:45:30Z")

</div>

> [@LiberalArtist](#):
>
> Maybe I want some kind of debugger or inspector for `syntax-parse` error messages?

There's no nice front end, but the [`syntax/parse/debug`](https://docs.racket-lang.org/syntax/Debugging_and_Inspection_Tools.html) module has some tools for debugging/inspecting how `syntax-parse` behaves. In particular, `debug-syntax-parse!` will tell `syntax-parse` to print out the internal data structures gathered during parsing that it uses to synthesize error messages. You should probably look at the comments in `(submod syntax/parse/private/residual progress)` to understand what the data structures mean.

The translation to error "prose" is in `syntax/parse/private/runtime-report`; it's complicated, but there are some comments about when error reporting discards specific information. I think the main thing you can rely on is that if all of the parse failures have a common prefix, you can rely on `syntax-parse` to report that common prefix. After the failures fork, you're at the mercy of heuristics. Your early version had two occurrences of `key:expr` in subpatterns with different descriptions. If there's a failure, it's going to fail in two different ways. In contrast, @notjack's first version has only one subpattern for the `key`, followed by an optional suffix.
