Implementing "splicing" syntax

Hello Racketeers,
I am working on a flow language (like a poor sibling of Qi) and realized that one of the things that would allow me to write a better structured code is some kind of "splicing" syntax macros. That is macros that - when expanded - get spliced in the surrounding expression.
Here is my current proof-of-concept:

#lang racket/base

(require (for-syntax racket/base
                     (for-syntax racket/base)))

(define-syntax (define-my-cases stx)
  (syntax-case stx ()
    ((_ id cases ...)
     #'(begin-for-syntax
         (define-syntax (id stx) #'(cases ...))))))

(define-syntax (expand-my-case stx)
  (syntax-case stx (else)
    ((_ _)
     #'(void))
    ((_ _ (else expr ...))
     #'(let ()
         expr ...))
    ((_ val ((tokens ...) expr ...) more ...)
     #'(if (memq val '(tokens ...))
           (let () expr ...)
           (expand-my-case val more ...)))
    ((_ val (token expr ...) more ...)
     #'(if (eq? val 'token)
           (let () expr ...)
           (expand-my-case val more ...)))
    ((_ val id more ...)
     #`(expand-my-case val #,@(expand-syntax-to-top-form #'(id)) more ...))))

(define-syntax (my-case stx)
  (syntax-case stx ()
    ((_ val-expr case0 more-cases ...)
     #'(let ((val val-expr))
         (expand-my-case val case0 more-cases ...)))))

(define-my-cases one23
  (1 410)
  ((2 3) 423))

(my-case
 1
 one23
 (4 54)
 ((5 6) 556)
 (else 666))
;; => 410

(my-case
 3
 one23
 (7 77)
 ((8 9) 789)
 (else 888))
;; => 423

(my-case
 4
 one23
 (4 54)
 ((5 6) 556)
 (else 666))
;; => 54

(my-case
 4
 one23
 (7 77)
 ((8 9) 789)
 (else 888))
;; => 888

Questions quickly arise though...

  1. Is this an optimal approach?
    Some help from (some kind of) expander is definitely needed, but should it be like this?
  2. Will it play well with macros with arguments?
    This boils down mainly to the scope of identifiers introduced in the splicing syntax definition. For example (not a working code):
(define-my-cases (one23 x y)
  (1 (* 410 x))
  ((2 3) (* 423 y)))

Any comments and/or suggestions are more than welcome.

2 Likes

So far the examples you show look like they would be best implemented by a runtime abstraction rather than a compile-time one. Do you really want to generate duplicate code for the expressions in the body of a define-my-cases definition for every use? Here's an alternative that makes define-my-cases define a function, instead of a macro:

#lang racket/base

(require (for-syntax racket/base
                     (for-syntax racket/base)))

(define no-match-value (gensym))

(define-syntax (define-my-cases stx)
  (syntax-case stx ()
    ((_ id cases ...)
     #'(define (id val)
         (my-case val
                  cases ...
                  (else no-match-value))))))

(define-syntax (expand-my-case stx)
  (syntax-case stx (else)
    ((_ _)
     #'(void))
    ((_ _ (else expr ...))
     #'(let ()
         expr ...))
    ((_ val ((tokens ...) expr ...) more ...)
     #'(if (memq val '(tokens ...))
           (let () expr ...)
           (expand-my-case val more ...)))
    ((_ val (token expr ...) more ...)
     #'(if (eq? val 'token)
           (let () expr ...)
           (expand-my-case val more ...)))
    ((_ val id more ...)
     #'(let ([res (id val)])
         (if (eq? res no-match-value)
         (expand-my-case val more ...)
         res)))))

(define-syntax (my-case stx)
  (syntax-case stx ()
    ((_ val-expr case0 more-cases ...)
     #'(let ((val val-expr))
         (expand-my-case val case0 more-cases ...)))))

(define-my-cases one23
  (1 410)
  ((2 3) 423))

(my-case
 1
 one23
 (4 54)
 ((5 6) 556)
 (else 666))
;; => 410

(my-case
 3
 one23
 (7 77)
 ((8 9) 789)
 (else 888))
;; => 423

(my-case
 4
 one23
 (4 54)
 ((5 6) 556)
 (else 666))
;; => 54

(my-case
 4
 one23
 (7 77)
 ((8 9) 789)
 (else 888))
;; => 888

It would be easy to generalize this to allow define-my-cases to accept arguments by having it expand to a curried function.

However, if you really do want to make your cases language macro-extensible, my syntax-spec language (syntax-spec-v1) is designed for creating custom macro extensible languages just like this. Here's how your example might look with syntax-spec:

#lang racket/base

(require syntax-spec
         (for-syntax racket/base
                     syntax/parse))

(syntax-spec
  (extension-class case-macro)

  (nonterminal token
    n:number)
  
  (nonterminal alternatives
    (t:token ...)
    t:token)
  
  (nonterminal case
    #:allow-extension case-macro
    (case-begin c:case ...)
    ((~literal else) rhs:expr ...)
    (a:alternatives rhs:expr ...))
  
  (host-interface/expression
   (my-case e:racket-expr
            c:case ...)
   #'(compile-cases
      e
      c ...)))

(define-syntax (compile-cases stx)
  (syntax-case stx (else case-begin)
    ((_ _)
     #'(void))
    ((_ _ (else expr ...))
     #'(let ()
         expr ...))
    ((_ val (case-begin cases ...) more ...)
     #`(compile-cases val
                      cases ...
                      more ...))
    ((_ val ((tokens ...) expr ...) more ...)
     #'(if (memq val '(tokens ...))
           (let () expr ...)
           (compile-cases val more ...)))
    ((_ val (token expr ...) more ...)
     #'(if (eq? val 'token)
           (let () expr ...)
           (compile-cases val more ...)))))

(begin-for-syntax
  (define (define-my-cases-transformer cases)
    (case-macro
     (lambda (stx)
       (syntax-case stx ()
         [name:id
          #`(case-begin . #,cases)])))))

(define-syntax (define-my-cases stx)
  (syntax-case stx ()
    ((_ id cases ...)
     #'(define-syntax id
         (define-my-cases-transformer #'(cases ...))))))

(define-my-cases one23
  (1 410)
  ((2 3) 423))

(my-case
 1
 one23
 (4 54)
 ((5 6) 556)
 (else 666))
;; => 410

(my-case
 3
 one23
 (7 77)
 ((8 9) (define x 789) x)
 (else 888))
;; => 423

(my-case
 4
 one23
 (4 54)
 ((5 6) 556)
 (else 666))
;; => 54

(my-case
 4
 one23
 (7 77)
 ((8 9) 789)
 (else 888))
;; => 888
``
1 Like

Yes, the code duplication is one of my concerns. The simple example doesn't capture the whole picture, which can be seen here:

The issue is that all the expressions in a step position must always return the updated value of the actor as it passes through the flow. That makes runtime dispatch a bit more tricky, but I will definitely give it yet another try before deciding which implementation suits my (mainly future) needs.
Also a nice simplification was suggested by @samth on Discord based on his paper [1106.2578] Extensible Pattern Matching in an Extensible Language - the usage of syntax-local-value:

#lang racket/base

(require (for-syntax racket/base))

(define-syntax (define-my-cases stx)
  (syntax-case stx ()
    ((_ (id args ...) cases ...)
     #'(define-syntax (id stx)
         (syntax-case stx ()
           ((id args ...)
            #'(cases ...)))))
    ((_ id cases ...)
     #'(define-syntax id
         #'(cases ...)))))

(define-syntax (expand-my-case stx)
  (syntax-case stx (else case)
    ((_ _)
     #'(void))
    ((_ _ (else expr ...))
     #'(let ()
         expr ...))
    ((_ val (case id args ...) more ...)
     #`(expand-my-case val #,@((syntax-local-value #'id) #'(id args ...)) more ...))
    ((_ val ((tokens ...) expr ...) more ...)
     #'(if (memq val '(tokens ...))
           (let () expr ...)
           (expand-my-case val more ...)))
    ((_ val (token expr ...) more ...)
     #'(if (eq? val 'token)
           (let () expr ...)
           (expand-my-case val more ...)))
    ((_ val id more ...)
     #`(expand-my-case val #,@(syntax-local-value #'id) more ...))))

(define-syntax (my-case stx)
  (syntax-case stx ()
    ((_ val-expr case0 more-cases ...)
     #'(let ((val val-expr))
         (expand-my-case val case0 more-cases ...)))))

(This new implementation also handles macro arguments that can be bound during expansion to any identifiers in the surrounding expression when used).
And thank you very much for the syntax-spec-v1 example. It's VERY tempting to start using it now :wink:
Btw, do I get it right that syntax-spec combines EBNF-like approach with syntax-parse-like approach?

1 Like

The definition of a language in syntax-spec does look a little EBNF-like, yes. But the real value of syntax-spec is that:

  1. It separates expansion from compilation, allowing the DSL compiler to analyze the entire expanded syntax.
  2. It correctly implements hygienic macro expansion, which can be tricky when your language has scoping and binding forms.

If your language has a simple syntax-directed compilation to Racket and you don't run into any hygiene issues, the approach using syntax-local-value may be perfectly adequate.

1 Like