Choose syntax-parse for new Racket DSL macros, keep syntax-case for base-only libraries
For new public DSL macros in Racket 8.x, prefer syntax-parse for clearer compile-time errors and lower maintenance cost, and keep syntax-case only for base-only or legacy code.
17 Jun 2026, 02:14 UTC

The decision you face with a new public DSL
You need a macro pattern language that gives users clear compile-time errors, is maintainable by a team with limited hygienic macro experience, keeps builds fast, and stays stable across Racket 8.x. For library code with a public API, the practical takeaway is to adopt syntax-parse for new DSL macros and reserve syntax-case for code that must stay in racket/base or maintain legacy compatibility.
Constraints that drive the choice
Public DSL macros are user-facing. Diagnostics matter more than micro-expansion cost. The team needs a pattern language with less manual boilerplate for complex forms. The API must be stable in Racket 8.x, where ellipsis and attribute syntax for syntax-parse is settled. Build speed is a concern, but macro expansion is a small fraction of total compile time for most libraries.
syntax-parse is not in racket/base. It lives in syntax/parse and requires an explicit require. That is a dependency cost. syntax-case is in racket/base, has minimal runtime cost, and gives maximal control, but error reporting and maintenance cost grow with pattern complexity.
Options comparison
| Criterion | syntax-parse | syntax-case |
|---|---|---|
| Error messages | Declarative, automatic, contextual | Manual, requires raise-syntax-error |
| Pattern power | Literals, ~and, ~or, ellipsis with guards | Core patterns, manual recursion |
| Learning curve | Higher initial, then faster for complex forms | Steeper long term, more boilerplate |
| Base dependency | Requires racket/syntax | In racket/base |
Trade-offs for library maintenance
syntax-parse gives better diagnostics and less boilerplate for complex patterns. Patterns are declarative, and the parser produces contextual errors without manual raise-syntax-error calls. Guards allow validation at expansion time. The cost is an explicit dependency on syntax/parse and a small compile-time overhead from the parser.
syntax-case is in base, has minimal overhead, and is maximally explicit. You control every step of matching and expansion. For simple macros, that control is fine. For a public DSL with optional keywords, repeated subforms, or multiple valid shapes, manual error handling becomes repetitive and error-prone.
For public APIs, diagnostics outweigh micro-performance. Team onboarding is faster with syntax-parse once the initial pattern concepts are learned, because the intent of the pattern is visible in the form itself.
Concrete implementation sketch
The following shows a minimal my-let macro using syntax-parse with a keyword and a guard. It parses (my-let ([x 1]) body...) and expands to (let ([x 1]) body...). Using #:literals and ~literal helps hygiene for the keyword.
(require (for-syntax syntax/parse))
(define-syntax (my-let stx)
(syntax-parse stx
[(_ ([id:id val:expr]) body:expr ...)
#:with id* (syntax/loc #'id id)
#'(let ([id* val]) body ...)]
[(_ bad ...)
(raise-syntax-error 'my-let "expected (my-let ([id val]) body ...)" stx)]))Key points: the pattern uses id:id and expr to classify subforms. The ellipsis handles repeated bodies. The guard is omitted for brevity; guards run at macro-expansion time, so avoid side effects or expensive computation there. The #:literals clause is not needed here because syntax-parse handles literals automatically, but ~literal can be used for explicit literal matching.
Limitations: syntax-parse is not in racket/base. Require syntax/parse explicitly to avoid missing dependency errors. Version-sensitive behavior of ellipsis and attribute syntax changed between Racket 7 and 8; assume Racket 8.x for examples. Guards execute during expansion, not runtime.
Validation without claiming test results
Confirm the library is available in the target installation. Run in a terminal where Racket is installed, no elevated permissions needed:
racket -e "(require syntax/parse)"Expected check: the command returns without a require error. Risk: older installations may lack the package, which would surface as a require failure.
Create a test file with the macro and inspect expansion. Example command with a placeholder file path:
raco expand --output /tmp/expanded.rkt path/to/test-file.rktInspect the expanded file for the expected let form. Compare error messages for malformed input between syntax-parse and syntax-case implementations to validate diagnostic quality. Malformed input such as missing binding should produce a contextual error pointing to the my-let use site with syntax-parse, versus a generic error when manually raised.
Rollback is not required for this decision because no state is changed; you can keep existing syntax-case macros and introduce syntax-parse only for new macros.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.