Implementation of the multi-id library
1 Syntax properties implemented by the defined multi-id
«props»1
«maybe-define-type»1
«maybe-define-type»2
«props»2
«props»3
«props»4
«props»5
«multi-id-body»
2 Signature of the multi-id macro
«multi-id»
«type-expander-kws»
«match-expander-kws»
«custom-write-kw»
«set!-transformer-kws»
«stx-class-kw-else»
«stx-class-kw-set!+call+id»
«fail-set!»
«fallback-kw»
«prop-keyword-syntax-class»
3 Tests for multi-id
«test-multi-id»1
«test-multi-id»2
«test-multi-id»3
4 Conclusion
«*»
8.17.0.6

Implementation of the multi-id library🔗ℹ

Suzanne Soy <racket@suzanne.soy>

This document describes the implementation of the multi-id library, using literate programming. For the library’s documentation, see the Polyvalent identifiers with multi-id document instead.

1 Syntax properties implemented by the defined multi-id🔗ℹ

The multi-id macro defines the identifier name as a struct with several properties:

The multi-id macro therefore defines name as follows:

(template
 (begin
   «maybe-define-type»
   (define-syntax name
     (let ()
       (struct tmp ()
         «props»)
       (tmp)))))

2 Signature of the multi-id macro🔗ℹ

The multi-id macros supports many options, although not all combinations are legal. The groups of options specify how the name identifier behaves as a type expander, match expander, how it is printed with prop:custom-write and how it acts as a prop:set!-transformer, which covers usage as a macro, identifier macro and actual set! transformer.

(begin-for-syntax
  «stx-class-kw-else»
  «stx-class-kw-set!+call+id»
  «prop-keyword-syntax-class»)
(define-syntax/parse (define-multi-id name:id
                       (~or «type-expander-kws»
                            «match-expander-kws»
                            «custom-write-kw»
                            «set!-transformer-kws»
                            «fallback-kw»)
                       )
  «multi-id-body»)

These groups of options are detailed below:

  • The #:type-expander, #:type-noexpand and #:type-expand-once options are mutually exclusive.

    (~optional (~or (~seq #:type-expander p-type:expr)
                    (~seq #:type-noexpand p-type-noexpand:expr)
                    (~seq #:type-expand-once p-type-expand-once:expr)))

  • The #:match-expander and #:match-expander-id options are mutually exclusive.

    (~optional (~or (~seq #:match-expander p-match:expr)
                    (~seq #:match-expander-id p-match-id:id)))

  • The #:custom-write keyword can always be used

    (~optional (~seq #:custom-write p-write:expr))

  • The prop:set!-transformer can be specified as a whole using #:set!-transformer, or using one of #:else, #:else-id, #:mutable-else or #:mutable-else-id, or using some combination of #:set!, #:call (or #:call-id) and #:id.

    (~optional (~or (~seq #:set!-transformer p-set!:expr)
                    :kw-else
                    :kw-set!+call+id))

    More precisely, the kw-else syntax class accepts one of the mutually exclusive options #:else, #:else-id, #:mutable-else and #:mutable-else-id:

    (define-splicing-syntax-class kw-else
      #:attributes (p-just-set! p-just-call p-just-id)
      (pattern (~seq #:mutable-else p-else)
               #:with p-just-set! #'#'(set! p-else . rest)
               #:with p-just-call #'#'(p-else . rest)
               #:with p-just-id #'#'p-else)
      (pattern (~seq #:else p-else)
               #:with p-just-set! «fail-set!»
               #:with p-just-call #'#`(#,p-else . rest)
               #:with p-just-id #'p-else)
      (pattern (~seq #:mutable-else-id p-else-id)
               #:with (:kw-else) #'(#:mutable-else #'p-else-id))
      (pattern (~seq #:else-id p-else-id)
               #:with (:kw-else) #'(#:else #'p-else-id)))

    The kw-set!+call+id syntax class accepts optionally the #:set! keyword, optionally one of #:call or #:call-id, and optionally the #:id keyword.

    (define-splicing-syntax-class kw-set!+call+id
      (pattern (~seq (~or
                      (~optional (~seq #:set! p-user-set!:expr))
                      (~optional (~or (~seq #:call p-user-call:expr)
                                      (~seq #:call-id p-user-call-id:id)))
                      (~optional (~or (~seq #:id p-user-id:expr)
                                      (~seq #:id-id p-user-id-id:expr))))
                     )
               #:attr p-just-set!
               (and (attribute p-user-set!) #'(p-user-set! stx))
               #:attr p-just-call
               (cond [(attribute p-user-call)
                      #'(p-user-call stx)]
                     [(attribute p-user-call-id)
                      #'(syntax-case stx ()
                          [(_ . rest) #'(p-user-call-id . rest)])]
                     [else #f])
               #:attr p-just-id
               (cond [(attribute p-user-id) #'(p-user-id stx)]
                     [(attribute p-user-id-id) #'#'p-user-id-id]
                     [else #f])))

    When neither the #:set! option nor #:set!-transformer are given, the name identifier acts as an immutable object, and cannot be used in a set! form. If it appears as the second element of a set! form, it raises a syntax error:

    #'(raise-syntax-error
       'self
       (format "can't set ~a" (syntax->datum #'self)))

  • As a fallback, for any #:xxx keyword, we check whether a corresponding prop:xxx exists, and whether it is a struct-type-property?:

    (~seq fallback:prop-keyword fallback-value:expr)

    The check is implemented as a syntax class:

    (define-syntax-class prop-keyword
      (pattern keyword:keyword
               #:with prop (datum->syntax #'keyword
                                          (string->symbol
                                           (string-append
                                            "prop:"
                                            (keyword->string
                                             (syntax-e #'keyword))))
                                          #'keyword
                                          #'keyword)
               #:when (eval #'(struct-type-property? prop))))

3 Tests for multi-id🔗ℹ

(define (p1 [x : Number]) (+ x 1))
 
(define-type-expander (Repeat stx)
  (syntax-case stx ()
    [(_ t n) #`(List #,@(map (λ (x) #'t)
                             (range (syntax->datum #'n))))]))
 
(define-multi-id foo
  #:type-expander
  (λ (stx) #'(List (Repeat Number 3) 'x))
  #:match-expander
  (λ (stx) #'(vector _ _ _))
  #:custom-write
  (λ (self port mode) (display "custom-write for foo" port))
  #:set!-transformer
  (λ (_ stx)
    (syntax-case stx (set!)
      [(set! self . _)
       (raise-syntax-error 'foo (format "can't set ~a"
                                        (syntax->datum #'self)))]
      [(_ . rest) #'(+ . rest)]
      [_ #'p1])))
 
(check-equal? (ann (ann '((1 2 3) x) foo)
                   (List (List Number Number Number) 'x))
              '((1 2 3) x))
 
;(set! foo 'bad) should throw an error here
 
(let ([test-match (λ (val) (match val [(foo) #t] [_ #f]))])
  (check-equal? (test-match #(1 2 3)) #t)
  (check-equal? (test-match '(1 x)) #f))
 
(check-equal? (foo 2 3) 5)
(check-equal? (map foo '(1 5 3 4 2)) '(2 6 4 5 3))

It would be nice to test the (set! foo 'bad) case, but grabbing the compile-time error is a challenge (one could use eval, but it’s a bit heavy to configure).

Test with #:else:

(begin-for-syntax
  (define-values
    (prop:awesome-property awesome-property? get-awesome-property)
    (make-struct-type-property 'awesome-property)))
 
(define-multi-id bar-id
  #:type-expander
  (λ (stx) #'(List `,(Repeat 'x 2) Number))
  #:match-expander
  (λ (stx) #'(cons _ _))
  #:custom-write
  (λ (self port mode) (display "custom-write for foo" port))
  #:else-id p1
  #:awesome-property 42)
 
(check-equal? (ann (ann '((x x) 79) bar)
                   (List (List 'x 'x) Number))
              '((x x) 79))
 
;(set! bar 'bad) should throw an error here
 
(let ([test-match (λ (val) (match val [(bar-id) #t] [_ #f]))])
  (check-equal? (test-match '(a . b)) #t)
  (check-equal? (test-match #(1 2 3)) #f))
 
(let ([f-bar-id bar-id])
  (check-equal? (f-bar-id 6) 7))
(check-equal? (bar-id 6) 7)
(check-equal? (map bar-id '(1 5 3 4 2)) '(2 6 4 5 3))
 
(require (for-syntax rackunit))
(define-syntax (check-awesome-property stx)
  (syntax-case stx ()
    [(_ id val)
    (begin (check-pred awesome-property?
                       (syntax-local-value #'id (λ _ #f)))
           (check-equal? (get-awesome-property
                          (syntax-local-value #'id (λ _ #f)))
                         (syntax-e #'val))
           #'(void))]))
(check-awesome-property bar-id 42)

(define-multi-id bar
  #:type-expander
  (λ (stx) #'(List `,(Repeat 'x 2) Number))
  #:match-expander
  (λ (stx) #'(cons _ _))
  #:custom-write
  (λ (self port mode) (display "custom-write for foo" port))
  #:else #'p1)
 
(check-equal? (ann (ann '((x x) 79) bar)
                   (List (List 'x 'x) Number))
              '((x x) 79))
 
;(set! bar 'bad) should throw an error here
 
(let ([test-match (λ (val) (match val [(bar) #t] [_ #f]))])
  (check-equal? (test-match '(a . b)) #t)
  (check-equal? (test-match #(1 2 3)) #f))
 
(check-equal? (bar 6) 7)
(check-equal? (map bar '(1 5 3 4 2)) '(2 6 4 5 3))

4 Conclusion🔗ℹ

«*» ::=
(require (only-in type-expander prop:type-expander define-type)
         (only-in typed/racket [define-type tr:define-type])
         phc-toolkit/untyped
         (for-syntax phc-toolkit/untyped
                     racket/base
                     racket/syntax
                     syntax/parse
                     syntax/parse/experimental/template
                     (only-in type-expander prop:type-expander)))
(provide define-multi-id)
 
«multi-id»
 
(module* test-syntax racket/base
  (provide tests)
  (define tests #'(begin «test-multi-id»)))