Defining a macro in a local scope?

I’m trying to define a macro whose expansion references a local variable.

Here’s my example:

struct Reference
    source::Union{Nothing, LineNumberNode}
    registry::Dict
    key
end

let
    r = Dict()
    macro ref(name)
        Expr(:call, :Reference, __source__, r, name)
    end
    @ref("foo")
end

I’d expect this to translate to

let
    r = Dict()
    Reference(<someLineNumberNode>, f, "foo")
end

but instead, I get the error

ERROR: syntax: macro definition not allowed inside a local scope
Stacktrace:
 [1] top-level scope
   @ none:1

In CommonLisp I could use MACROLET. Does Julia have anything analogous to this?

Thanks.

Why?

GitHub - MarkNahabedian/AnotherParser.jl allows for a BNF to be constructed as a tree of structs and for that tree to drive the parsing of an input string.

The Dict represented by r maps non-terminals in a BNF grammar to the syntax associated with that non-terminal. The Reference struct allows for delayed, indirect substitution of a BNF subtree for a specified non-terminal name.

An example grammar that implements the SemVer version syntax is implemented in AnotherParser.jl/test/SemVerBNF.jl at master · MarkNahabedian/AnotherParser.jl · GitHub.

I can construct a working parser using my struct definitions, but the grammar is diccicult to debug because the structs are not tagged with their source locations.

I have a macro which, for each such struct definition, rewrites the struct to

add a `source` field;

rewrites its constructors to support that field;

defines a macro with the same name as the struct that invokes the constructor with __source__ filled in.

That macro is defined in AnotherParser.jl/src/note_BNFNode_location.jl at master · MarkNahabedian/AnotherParser.jl · GitHub

The BNF node definitions themselves are defined in AnotherParser.jl/src/BNFtypes.jl at master · MarkNahabedian/AnotherParser.jl · GitHub.

I could define a macro which takes the entire grammar and constructs the node tree from that. This would allow me to do a tree walk to replace the shorthand for all of my Reference nodes, but I don’t see how to avoid a tree walk that I fdeel the compiler should do for me were I allowed to define macros in a local scope.

Macros in Julia are only allowed to be defined in the global scope.

What happens if you define this macro globally?

You can also try defining the macro in a new module.

Thanks for your quick response.

I don’t see how I can define the macro globally since it depends on a local variable that would not exist in the global context.

The local function that the macro is intended to replace is a convenience so that I don’t explicitly need to pass the local variable whose value is the Dict every time I call the Reference constructor. The purpose of the macro is to include a source location as well.

In that case you can just use LineNumberNode(@__LINE__, @__FILE__) instead. (Note that the __source__ in your macro definition above will just end up evaluating to nothing. You probably wanted to put it inside a QuoteNode.)

Wait, Common Lisp’s macrolet doesn’t allow you to depend on a local variable, right? How would you write it in CL?

What good would it be otherwise?

foo.lisp:

(defclass reference ()
  ((dict :initarg :dict
         :reader reference-dict)
   (name :initarg :name
         :reader reference-name)))

(defmethod value ((r reference))
  (gethash (reference-name r) (reference-dict r)))


(defvar dict1 (make-hash-table))
(setf (gethash 'foo dict1) 7)
(defvar r1 (make-instance 'reference :dict dict1 :name 'foo))

(assert (eq (value r1) 7))


(let ((x (make-hash-table)))
  (print x)
  (macrolet ((ref (name)
                  `(make-instance 'reference
                                  :dict x
                                  :name ,name)))
     (describe (ref `bar))
     ))
* (load "c:/Users/Mark Nahabedian/foo.lisp")

#<HASH-TABLE :TEST EQL :COUNT 0 {1002B4D393}> 
#<REFERENCE {1002BC4C63}>
  [standard-object]

Slots with :INSTANCE allocation:
  DICT                           = #<HASH-TABLE :TEST EQL :COUNT 0 {1002B4D393}>
  NAME                           = BAR
T
* 

It’s been a long time. My lisp is really rusty.

Yeah, but it would work just as well if defined as a global macro, no? Something like

(macro ref (name)
     `(make-instance 'reference
                     :dict x
                     :name ,name)))

(let ((x (make-hash-table)))
  (print x)
  (describe (ref `bar))))

The x in your macro expansion is just a symbol, it’ll macroexpand fine in any context (though it’ll be a compile-time error if no x is in scope).

You can do the same in julia. Although of course, MACROLET is a lot more elegant than a global macro, no argument there.

I do believe there are some cases that can’t be translated into julia. Macros whose expansions contain MACROLET come to mind… I think you basically have to use MacroTools.prewalk/postwalk for those.

Thanks. I stand corrected.

From the CommonLisp Hyperspec

The macro-expansion functions defined by macrolet are defined in the lexical environment in which the macrolet form appears. Declarations and macrolet and symbol-macrolet definitions affect the local macro definitions in a macrolet, but the consequences are undefined if the local macro definitions reference any local variable or function bindings that are visible in that lexical environment.

I’ll need to rething this.

Come to think of it, with MacroTools.postwalk, one could certainly write MACROLET and SYMBOL-MACROLET. That’d be useful, potentially.