Search code examples
hy

Expanding macro results in KeyError


With the following (simplified) code:

(setv agnostic-manager-installers {})

(defmacro alias-assign [am &rest aliases]
    (for [alias aliases] (assoc
        agnostic-manager-installers
        (str alias)
        (-> (globals) (get (str am)) (get "install")))))

(setv brew {
    "prefix" "/home/linuxbrew/.linuxbrew/"
    "install" (defn brew [] (run
                "curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | sudo bash"
                :shell True))
})

(alias-assign brew brew homebrew home-brew linuxbrew linux-brew)

I'm getting the following error:

Traceback (most recent call last):
  File "/home/shadowrylander/.local/syvl/python/hy/bin/hy", line 8, in <module>
    sys.exit(hy_main())
  File "/usr/lib/python3.9/contextlib.py", line 137, in __exit__
    self.gen.throw(typ, value, traceback)
  File "<stdin>", line 9, in alias_assign
hy.errors.HyMacroExpansionError: 
  File "<stdin>", line 20
    (alias-assign brew brew homebrew home-brew linuxbrew linux-brew)
    ^--------------------------------------------------------------^
expanding macro alias-assign
  KeyError: 'brew'

I thought the macro was not supposed to evaluate the arguments until compile-time, if I'm reading the error correctly (which I don't think I am)? Basically, I would like to not write the double quotes around every single alias provided to alias-assign, which is why I went with a macro.


Solution

  • Here's some much simpler code that produces the same error:

    (setv brew 1)
    
    (defmacro m []
      (get (globals) "brew"))
    
    (m)
    

    Perhaps the problem is more obvious now: trying to access the global variable brew during macro-expansion doesn't work because brew doesn't exist during compile-time, when macro-expansion happens. The same problem occurs, with NameError instead of KeyError, if you just say brew instead of (get (globals) "brew"). In any case, the form (setv brew 1) won't be evaluated until run-time. One way around this is to say (eval-when-compile (setv brew 1)) instead. This makes the evaluation happen earlier.

    A broader problem is that you seem to be executing code that you actually want to return, as the result of the macro expansion. After all, the body of your macro is a for, so it will always return None. Contrast with the following code, which uses quoting and unquoting to generate forms and return them (and uses updated syntax):

    (setv agnostic-manager-installers {})
    
    (defmacro alias-assign [am #* aliases]
      `(do ~@(gfor
        alias aliases
        `(setv (get agnostic-manager-installers ~(str alias))
          (get ~am "install")))))
    
    (setv brew (dict
      :prefix "/home/linuxbrew/.linuxbrew/"
      :install "placeholder"))
    
    (alias-assign brew brew homebrew home-brew linuxbrew linux-brew)
    
    (print (hy.repr agnostic-manager-installers))
    

    The result is:

    {"brew" "placeholder"  "homebrew" "placeholder"  "home-brew" "placeholder"  "linuxbrew" "placeholder"  "linux-brew" "placeholder"}