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:
Second, you're assigning a nonlocal variable there, `res`. I don't know if you were doing that on purpose, but here's a version that avoids doing that:
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:
arc> (let a 1 (w/tab (obj b 2) (+ b b)))
4
arc> (let a (obj b 2) (w/tab a (= b 4)) a)
#hash((b . 4))
arc> (let a 1 (w/tab (obj b '(+ (+ 2 y) 3)) (join b b)))
(+ (+ 2 y) 3 + (+ 2 y) 3)
---
As I mentioned above, there's one case that's sadly impossible to support without hacking on the language a bit:
arc> (let a 1 (w/tab (obj b 2) (+ a b)))
Error: "reference to undefined identifier: _a"
Okay, here's a really hackish way to add this capability to the language.
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`.
Arc's own codebase doesn't implement comments of any sort, but it relies on Racket's line comments, so those at least are reliable across Arc implementations.
; A multi-line comment (nestable):
#|foo bar
baz|#
; A commented-out s-expression:
#;(foo bar
baz)
; A commented-out s-expression which happens to be a string:
#;"foo bar
baz"
; A commented-out s-expression which happens to be a symbol:
#;|foo bar
baz|
; A shebang comment
#! foo bar \
baz
That version of `atom?` always returns nil, so I'll offer a correction.
Here's a very direct transliteration:
(= atom?
(fn (x)
(and (no (acons x)) (no (no x)))))
The equivalents for `not` and `null?` in Arc are both `no`, because the empty list is the only falsy value. Scheme uses dedicated values for true and false, `t` and `#f`, but Arc uses the symbols `t` and `nil`, and `nil` doubles as the empty list.
Here's a version that uses the utilities Arc provides out of the box:
I don't know if this is the same as what you're seeing, but there's a feature of Arc application servers that does this. It's meant for rate-limiting people who might be attempting denial-of-service attacks against the site:
The default settings throttle anyone who's doing 30 requests in 10 seconds. Each page load does 5 or 6 requests to get images and such, so we can get throttled if we reload or follow links 7 times in 10 seconds. When I try it on Arc Forum, it seems to be doing exactly that.
At Hacker News... if I go a few pages into the recent comments and hit F5 approximately 30 times in 10 seconds, I get an Nginx error page. Hacker News probably lets Nginx serve its static files rather than having every request go through the Arc server's throttle, but otherwise its configuration seems to be pretty similar.
Maybe you're navigating 7 times in 10 seconds by hand, but there are other possibilities too. If you're on the same external IP address as others who are accessing the Arc Forum, you might all be under a single rate limit.
Hmmm, that's interesting but I don't think it's what I'm running into. I get it on a first visit. I don't think my housemates are visiting the arc-forum at all . . .
Yeah! It might be a little confusing for people who lurk here and don't follow the discussion into the Slack, but I think a Slack would open up some communication opportunities.
I wonder if anyone would like to try writing a Slack bot in Arc. :)
If the Arc Forum adds a link to anything, I hope it adds a link to the community-maintained Arc website (https://arclanguage.github.io/). We can add links to other resources from there.
The error has to do with looking up the variable "Rtl". The "R" probably comes from a bug in Racket's reader where certain things pasted into a Windows terminal window are parsed incorrectly. Where you typed "(tl)", somehow it saw "Rtl)".
In my experience, the bug doesn't occur as long as the text I'm pasting has a newline at the end. Maybe you could try that. That is, instead of just selecting the text you want to copy, select a blank line after it too.
Alternatively, you could type everything by hand instead of pasting... but that sounds pretty painful. Hopefully you don't have to resort to that.
EDIT: I tried to reproduce that pasting issue with Racket 6.4 on Windows 10, and I don't get it anymore. You're using Racket 6.4 too, so are you using a particular version of Windows?
Well... the axioms of Arc are enumerated in the source code. But that might not be satisfying unless you think Racket is a simple metatheory. :)
Even mathematical axiom systems are always in terms of some metatheory, and the metatheory itself might be in doubt. There's no right answer.
That said, Arc's implementation in terms of Racket is rather large and ad-hoc, and Racket's own implementation is rather large and ad-hoc. There are nicer foundations than these already, like Martin-Löf type theory.