10 Implementation Examples🔗

The prototype Rhombus implementation starts with a #%module-begin form that takes a shrubbery sequence wrapped with top as its input. Simplifying somewhat, the implementation uses a rhombus-top helper macro:

(define-syntax (rhombus-module-begin stx)
  (syntax-parse stx
    #:datum-literals (top)
    [(_ (top . content))
        (rhombus-top . content))]))

The rhombus-top macro tries to parse each top-level form as either a declaration, definition, or expression. Parsing a declaration or definition produces sequence of terms as the parsed attribute, while parsing an expression produces a single expression as parsed:

(define-syntax (rhombus-top stx)
  (syntax-parse stx
    [(_) #'(begin)]
    [(_ form . forms)
         #,(syntax-parse #'form
             [e::declaration #'(begin . e.parsed)]
             [e::definition #'(begin . e.parsed)]
             [e::expression #'(#%expression e.parsed)])
         (rhombus-top . forms))]))

This rhombus-top macro uses a typical trampolining pattern: the Racket macro expander will perform any declaration or definition bindings before expanding the recursive use of rhombus-top. which will then force more declaration, definition, and expression parsing. That way, Rhombus-level operators can be defined and then used in the same module.

The :definition syntax class is defined using the simplified Rhombus expander API:

  #:syntax-class :definition
  #:desc "definition"
  #:transformer-ref definition-transformer-ref
  #:check-result check-definition-result)

Here, definition-transformer-ref refers to a function that extracts a transformer structure from a compile-time value (returning #f if no such structure is available). The check-definition-result function makes sure that the low-level transformer returns at least a list-shaped syntax object, but that’s just for earlier error detection.

A simple implementation of the def definition form could be like this:

(define-syntax def
    (lambda (stx)
      (syntax-parse stx
        #:datum-literals (block)
        ; match `def <binding>: <form> ...`, where
        ; `:` grouping is represented by `block`
        [(_ b::binding (block rhs ...))
         (build-values-definitions #'b.parsed
                                   #'(rhombus-block rhs ...))]))))

Parsing b as :binding produces a parsed attribute that embeds the identifiers to bind as well as predicates and conversion to apply to the result of the right-hand side. The right-hand side is put into a rhombus-block form, which bounces back to (local) definition and expression parsing, roughly like this:

(define-syntax (rhombus-block stx)
  (syntax-parse stx
    [(_ . tail) #`(let () (rhombus-body . tail))]))
(define-syntax (rhombus-body stx)
  (syntax-parse stx
    [(_) #'(begin)]
    [(_ e::definition . tail)
         (begin . e.parsed)
         (rhombus-body . tail))]
    [(_ e::expression . tail)
         (#%expression e.parsed)
         (rhombus-body . tail))]))

Here’s the definition of the :expression syntax class:

  #:syntax-class :expression
  #:prefix-more-syntax-class :prefix-op+expression+tail
  #:infix-more-syntax-class :infix-op+expression+tail
  #:desc "expression"
  #:operator-desc "expression operator"
  #:in-space in-expression-space
  #:name-path-op '|.|
  #:prefix-operator-ref expression-prefix-operator-ref
  #:infix-operator-ref expression-infix-operator-ref
  #:check-result check-expression-result
  #:make-identifier-form make-identifier-expression)

Expressions use the default mapping space, so in-expression-space is just the identity function. The expression-prefix-operator-ref and expression-infix-operator-ref accessors are analogous to definition-transformer-ref, but for expression prefix and infix operators.

An infix expression operator like + is defined roughly like this:

(provide (rename-out [rhombus+ +])) ; and similar for `rhombus-`, etc.
(define-syntax rhombus+
  (expression-infix-operator #'rhombus+
                             (list (cons #'rhombus* 'weaker)
                                   (cons #'rhombus/ 'weaker)
                                   (cons #'rhombus- 'same))
                             (lambda (form1 form2 stx)
                                ; this is where we compile to Racket's `+`:
                                (quasisyntax/loc stx (+ #,form1 #,form2)))

The actual implementation has more layers of abstraction, deals with macro scope introductions, supports a define*-like forward definition form, implements more complicated syntax, and so on. Some part of the language would be built in this low-level way, including operator- and macro-defining forms like operator and expr.macro, and then more of Rhombus could be built using Rhombus.