The essential trouble you'll have with this macro is that Arc macros don't know what variables are in their caller's lexical environment.
(let a 1
(w/tab (obj b 2)
(+ a b)))
Since the place `b` will be looked up isn't determined unil run time, it has to use `eval`. But a call to `eval` always uses the global scope, so we lose `a`.
We could work around this by changing the way Arc's macroexpander worked. For now, let's not worry about that. I'll return to this later.
I see some particular bugs that can be fixed in your code.
First, you're inserting the bound values as flattened expressions in your generated code, so they'll actually be flattened executed. That is, if you write (w/tab (obj x '(+ (+ 2 y) 4)) ...), then I think `x` will be bound to the function `+`, `+` will be bound to 2, and `y` will be bound to 4.
If that's not what you want, we can make this change:
Third, as you observed, the (eval ...) inside the (each ...) throws an error. What's going on is that `eval` evaluates things in the global scope, and you're trying to get a local variable. I recommend taking out the (each ...) loop and replacing it with a sequence of assignments. This means the keys you're assigning to will be determined based on the initial state of the table, rather than the final state, but that corresponds to the local variables that have been made anyway.
Fourth, this code evaluates the `ht` expression each and every time it does an assignment. This might be okay, except it's evaluating the expression in the local scope the first time and the global scope all the other times, which will probably lead to annoying errors. Let's evaluate it only once, in the local scope:
I'm doing something tricky there: I'm inserting the table itself as a quoted value. It's possible to avoid this with a pattern like ((eval `(fn (x) (... x ...))) x), but Arc lets us just do (eval `(... ',x ...)).
Er, actually, something's making that code not work on Anarki, even though it does work on Arc 3.1. Embedding a function (instead of a table) works on Anarki, so let's do that:
We have effectively called `eval` in a local scope. We constructed the local scope table explicitly and then explicitly set it up again in the generated code.
If anyone would like to incorporate that six-line hack I demonstrated into Anarki, I think it will be a bit nicer if (get-latest-macex-env) were dynamically scoped rather than permanently mutated on each call. This will only ever matter if a macro invokes `eval`, `macex`, or `macex1` during its own macroexpansion, but hey, it could happen!
(This is effectively very similar to Kernel fexprs. The similarity would be even stronger if the environment were a table that mapped each variable to its local macroexpander. Arc doesn't have locally scoped macros, but I think this is a good way to go if we want them.)
Wow great post, thanks for the reply. I didn't know eval was always executed at global scope, and I was not thinking about the ht variable being executed multiple times at different scope, so thanks for pointing that out. The rest of your explanation about how to get it working is very clear and I will be a better arc hacker for it!
The occasion for this macro is an object system built around closures and hash tables. I will post more about it when I've made more progress.
It would take a bit of refactoring: Changing the `env` list to a table, adding it as a parameter of `ac-macro?` (which should now look things up from that table first), and finally adding an `env` parameter to Arc's `eval`.