Arc Forumnew | comments | leaders | submitlogin
Macros not quite first-class? + small fix to ac-call
5 points by vincenz 6174 days ago | 6 comments
If I understand it correctly, macros are meant to be first-class.

This is how we can define a macro directly:

  (set foo (annotate 'mac (fn (x) x)))
If, let's go more basic:

    arc> (def id (x) x)
    #<procedure: id>
    arc> (set foo (annotate 'mac id))
    #3(tagged mac #<procedure: id>)
    arc> (foo 1)
    1
Interestingly, if you show foo:

    arc> foo
    #3(tagged mac #<procedure: id>)
Similarly:

    arc> (annotate 'mac id)
    #3(tagged mac #<procedure: id>
Then.. how come this doesn't work?

    arc> ((annotate 'mac id) 1)
    Error: "Function call on inappropriate object #3(tagged mac #<procedure: id>) (1)"


4 points by vincenz 6174 days ago | link

Seems that to get first-class macros one would have to:

    1) Not evaluate arguments directly in ac-call, but keep the original arguments and envs
    2) Add a clause in ar-apply to deal with macros
    3) Make sure that in ar-apply, arguments are evaluated for the case where it is still a function/list/hashtable and not a macro
Basically use lazyness only until we find out we're calling a macro.

I'd implement it, but I'm not certain pg wants to go this direction. I don't think it'd change anything else to the language even though under-the-hood it uses some lazy techniques.

-----

2 points by vincenz 6174 days ago | link

I finally found the bug, the problem is that no matter how you pitch it, you have to go through 'eval' to get an actual tagged value, and since eval is done per top-form, you'll never get the first form (annotate 'mac id) to evaluate and form a macro-value to use on 1. Shame :|

While exploring this, I modified the code some more, notice that w.r.t. to the other suggestion I made to ac-call (which was completely backwards compatible), the ac-call here changes only by -1- letter.

    (define (ac-call fn args env)
      (let ((afn (ac fn env))
            (macfn (ac-macro? afn)))
        (cond (macfn
               (ac-mac-call macfn args env))
              ((and (pair? fn) (eqv? (car fn) 'fn))
               `(,afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 0)
               `(ar-funcall0 ,afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 1)
               `(ar-funcall1 ,afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 2)
               `(ar-funcall2 , afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 3)
               `(ar-funcall3 ,afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 4)
               `(ar-funcall4 ,afn ,@(map (lambda (x) (ac x env)) args)))
              (#t
               `(ar-apply ,afn
                          (list ,@(map (lambda (x) (ac x env)) args)))))))
    
    ; returns #f or the macro function
    (define (ac-macro? fn)
      (let ((v
              (if (symbol? fn)
                (namespace-variable-value fn
                                          #t
                                          (lambda () #f))
                fn)))
        (if (and v
                 (ar-tagged? v)
                 (eq? (ar-type v) 'mac))
          (ar-rep v)
          #f)))

-----

2 points by vincenz 6174 days ago | link

Seems the problem is with ac-macro?

Debugging a bit, it seemed this gave #f for the second case, cause it's not a symbol. Simply replacing ac-macro? with something that just keeps 'fn' when it's not a symbol does not work. And then I realised why, if 'fn' is not a symbol, then it is still some input arc-code.

And this poses a big problem, one reason why I don't think macros will ever be first-class fully in this system. The problem is that you could have an arbitrary expression as first parameter and the only way to know whether it's a macro or not is to run it to its end.

This same issue is showing also with 'set which seems to be special. For instance, if one plays with acompile to compile arc-code, the crack starts to show. Normally, compiling should not have side-effects, simply compile to scheme code from arc code. However, now I see why macros work the way they work, 'set' code runs right off the bat. Simply try to compile the following code:

    (set do (annotate 'mac
              (fn args `((fn () ,@args)))))
    
    (set def (annotate 'mac
                (fn (name parms . body)
                  `(do (sref sig ',parms ',name)
                       (set ,name (fn ,parms ,@body))))))
    (set mac (annotate 'mac
               (fn (name parms . body)
                 `(do (sref sig ',parms ',name)
                      (set ,name (annotate 'mac (fn ,parms ,@body)))))))
    
    (def id (x) x)
    (def pr args
      (disp (car args))
      (car args))
    
    (set foo (pr (annotate 'mac id)))
You will see that it will print at compile time.

-----

2 points by vincenz 6174 days ago | link

While digging through the code to try and fix this bug, I did come up with the following cleanup for ac-call:

    (define (ac-call fn args env)
      (let* ((afn (ac fn env))
            (macfn (ac-macro? fn)))
        (cond (macfn
               (ac-mac-call macfn args env))
              ((and (pair? fn) (eqv? (car fn) 'fn))
               `(,afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 0)
               `(ar-funcall0 ,afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 1)
               `(ar-funcall1 ,afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 2)
               `(ar-funcall2 , afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 3)
               `(ar-funcall3 ,afn ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 4)
               `(ar-funcall4 ,afn ,@(map (lambda (x) (ac x env)) args)))
              (#t
               `(ar-apply ,afn
                          (list ,@(map (lambda (x) (ac x env)) args)))))))

-----

1 point by vincenz 6174 days ago | link

Refactoring some more:

    (define (ac-call fn args env)
      (let ((macfn (ac-macro? fn)))
        (if macfn
          (ac-mac-call macfn args env)
          (let ((afn (ac fn env))
                (aargs (map (lambda (x) (ac x env)) args))
                (nargs (length args)))
            (cond 
              ((and (pair? fn) (eqv? (car fn) 'fn))
               `(,afn ,@aargs))
              ((and (>= nargs 0) (<= nargs 4))
               `(,(string->symbol (string-append "ar-funcall" (number->string nargs)))
                                  ,afn ,@aargs))
               (#t
                `(ar-apply ,afn (list ,@aargs))))))))

-----

2 points by vincenz 6174 days ago | link

As a last food for thought: Is it really a good idea to 'eval' code when 'compiling' it? Shouldn't there be some clean way to stage it then?

-----