Lech Napierała
Lech Napierała

Reputation: 43

Racket: Macro outputs something weird instead of an array

My aim is to populate an array in compile phase (i.e. in a macro), and use it in execution phase. For some reason, though, object returned by a macro is not recognized by Racket as an array. To illustrate the problem, shortest code showing this behaviour:

(require (for-syntax math/array))
(require math/array)

(define-syntax (A stx)
  (datum->syntax stx `(define a ,(array #[#[1 2] #[3 4]]))))

(A)

After execution of this macro, 'a' is something, but I don't know what it is. It is not an array ((array? a) -> #f) nor a string, array-ref is not working on it, obviously, but it prints as: (array #[#[1 2] #[3 4]]). "class-of" from the "swindle" module claims it is "primitive-class:unknown-primitive", for what it's worth.

I have tried outputting a vector instead of an array, but it works as expected, i.e. resulting value is a vector in execution phase.

I have tried using CommonLisp style defmacro from "compatibility" module, thinking that this may have something to do do with datum->syntax transformation, but this changed nothing.

I have tested this on Win7 with Racket 6.5 and 6.7, as well as on Linux with Racket 6.7 - problem persists.

Any ideas?


update

Thanks to great answers and suggestions, I came up with following solution:

(require (for-syntax math/array))
(require math/array)

(define-syntax (my-array stx)
  (syntax-case stx ()
    [(_ id)
     (let
         ([arr (build-array
                #(20 20)
                (lambda (ind)
                  (let
                      ([x (vector-ref ind 1)]
                       [y (vector-ref ind 0)])
                    (list 'some-symbol x y (* x y)))))])
       (with-syntax ([syn-arr (eval (read (open-input-string (string-append "#'" (format "~v" arr)))))])
         #'(define id syn-arr)))]))

(my-array A)

I'm not sure if this is proper Racket (I welcome all suggestions on code improvement) but here is how it works:

Array is built and stored in "arr" variable. It is then printed to string, prepended with #' (so that this string represents syntax object now) and evaluated as code. This effectively converts array to syntax object, that can be embedded in macro output.

Advantage of this approach is, that every object that can be written out and then read back by Racket can be output by macro. Disadvantage is, that some objects can't (I'm looking at you, custom struct!) and therefore additional string-creating function may be required in some cases.

Upvotes: 2

Views: 126

Answers (1)

Alexis King
Alexis King

Reputation: 43902

First of all, don’t use datum->syntax like that. You’re throwing away all hygiene information there, so if someone was using a different language where define was called something else (like def, for example), that would not work. For a principled introduction to Racket macros, consider reading Fear of Macros.

Second of all, the issue here is that you are creating what is sometimes known as “3D syntax”. 3D syntax should probably be an error in this context, but the gist is that there is only a small set of things that you can safely put inside of a syntax object:

  • a symbol
  • a number
  • a boolean
  • a character
  • a string
  • the empty list
  • a pair of two pieces of valid syntax
  • a vector of valid syntax
  • a box of valid syntax
  • a hash table of valid syntax keys and values
  • a prefab struct containing exclusively valid syntax

Anything else is “3D syntax”, which is illegal as the output of a macro. Notably, arrays from math/array are not permitted.

This seems like a rather extreme limitation, but the point is that the above list is simply the list of things that can end up in compiled code. Racket does not know how to serialize arbitrary things to bytecode, which is reasonable: it wouldn’t make much sense to embed a closure in compiled code, for example. However, it’s perfectly reasonable to produce an expression that creates an array, which is what you should do here.

Writing your macro more properly, you would get something like this:

#lang racket

(require math/array)

(define-syntax (define-simple-array stx)
  (syntax-case stx ()
    [(_ id)
     #'(define id (array #(#(1 2) #(3 4))))]))

(define-simple-array x)

Now, x is (array #[#[1 2] #[3 4]]). Note that you can remove the for-syntax import of math/array, since you are no longer using it at compile time, which makes sense: macros just manipulate bits of code. You only need math/array at runtime to create the actual value you end up with.

Upvotes: 4

Related Questions