I've never been quite comfortable with how macros work under the hood. That's the root cause of this bug.

I have a macro that macroexpands into code containing, among other things, another macro call. That's fine. One of the arguments the inner macro expects is a symbol. However, I want this symbol to be generated based on input to the outer macro. I've reduced my code to this test case to show what I want.

Here's a function that takes inputs and simply combines them into a symbol:

Calculating it outside of the quasiquote works, but I'm uncomfortable, because I'm not 100% sure when exactly things are evaluated: generate-symbol is run at macroexpansion-time, right? But then how does that interact with having different values passed to this new fancy-combine

Similarly, we can edit fancy-combine2 slightly to have a nested quasiquote, and this works, but it feels hacky to me. I don't like having the nested quasiquotes right next to the symbols, but I can't quite explain why. It just feels like a code smell.

Hence, you get an error about unquote being undefined, because Arc doesn't actually define quasiquote or unquote as macros. They're just special identifiers that are basically parsed away before evaluation (assuming quasiquotes/unquotes are balanced). However, loading qq.arc defines an unquote macro, so instead of the error you had, you'll get this:

arc> (fancy-combine2 a b)
Error: "unquote not allowed outside of a quasiquote: a"

For some reason, I had in my head that, inside quasiquote in a macro, you need to unquote every variable you want to evaluate before returning the code to be executed. Note that all my examples have ,sym1 and ,sym2 (except fancy-combine4, but that's deliberately executing the code before the returned generated code).

I guess the moral of the story here is that ,(...) evaluates the entire sexp, and puts the value there. I'll have to do more reading about macros.

I guess the moral of the story here is that ,(...) evaluates the entire sexp, and puts the value there.

Yup! If your quasiquoted expression only contains unquotes, the expansion is simple. Basically,

`(sexp1 sexp2 ... ,sexpn ...)

will expand into

(list 'sexp1 'sexp2 ... sexpn ...)

This also applies recursively, so that

`(sexp1 (subexpr1 ,subexpr2))

expands into

(list 'sexp1 (list 'subexpr1 subexpr2))

With unquote-splicing, you can think of quasiquote expanding into cons, so

`(sexp1 sexp2 ,@sexp3)

will expand into

(cons (list 'sexp1 'sexp2) sexp3)

If you're interested, I encourage you to look over my implementation of quasiquote in Arc. It's actually pretty simple (175 lines with comments), and might help clarify how quasiquotation works.

I'll have to do more reading about macros.

I think you seem to have the mental model for macros down. And I've always said that macros aren't that magical, because they fall out as a natural consequence of the language. Hell, here's a simple Arc "interpreter" that shows how macros are handled differently from functions:

Here, interpret is really just another name for eval. Macros are simply functions whose inputs are unevaluated code. They construct some other bit of unevaluated code, and that result gets evaluated.

Quasiquote is then just a nice way to construct lists, whether it's unevaluated code for a macro to return or whatever. Its syntax takes some getting used to, but it comes with practice. Happy hacking!