openlang

Most of my macros turned into const and static

Nim makes macros easy to write. That's the problem.

Before reaching for macro, check the cheaper tools. A const is evaluated at compile time by the same VM that runs macros, so a lookup table built by an ordinary proc is just const table = buildTable(). A static: block runs code during compilation and generates nothing. Checks on config, sizes, string formats. Ordinary procs, called early.

Then templates. Plain substitution, no AST walking. Covers most tiny DSL wishes.

What's left for a real macro is syntax the other three can't express. Even then the macro only rearranges nodes. Logic that doesn't need NimNode lives in ordinary procs, and those I can test at runtime like anything else.

Small macro, big proc. Error messages stay readable.

The conversation continues 4

Follow the reasoning
  1. Permalink

    The reason to keep logic out of the expander is redefinition. A macro that expands into a call to an ordinary function keeps its expansion small, and the function can be redefined and tested without touching any caller. If the logic sits inside the expander instead, every site that used the macro holds whatever the expander produced when that site was compiled. Changing the logic then means finding and recompiling those sites, and a reader of the program has no record of which ones they are. Common Lisp code tends to follow this division for exactly that reason.

  2. Permalink

    Half agree. In C++ the template is both the macro and the generic type, so "keep it small" isn't really on offer. What you can do is move work out of it. Most old metaprogramming is now constexpr functions, which are just functions, loops and ifs included, evaluated at compile time when the context asks. consteval if you want to insist.

    Picking an overload by what a type can do got concepts. template <std::integral T> fails at the call site with a note about the unmet constraint instead of three screens of substitution failure from deep inside a library. Not always short. Shorter.

  3. Permalink

    A historical footnote. CoffeeScript was, in a sense, one big fixed macro over JavaScript. Fat arrows, classes, destructuring, default arguments, splats, string interpolation. Nobody could add sugar of their own, the compiler shipped all of it. Then most of it was absorbed into the language with ES2015, sometimes with subtly different semantics (CoffeeScript's => captured this from the enclosing scope in the compiled code, while an ES arrow has no this binding of its own, which looks the same from outside and isn't the same mechanism).

    That's my case for small macros from the other direction. Sugar that proves itself tends to get promoted into the language. A small macro is cheap to migrate off when that happens.

  4. Permalink

    There's no compile-time sublanguage in Agda to keep small, which sounds great until you pay for it. Type checking normalizes terms, so compile-time evaluation is just evaluation, and anything total can run there. The price is that totality gets checked. The termination checker wants recursion on structurally smaller arguments, and a perfectly reasonable function that recurses on n / 2 is rejected until you rewrite it with well-founded recursion or a fuel argument. Honestly that rewrite can come out longer than the function.

    Agda does have macros, through reflection, and I almost never want one. When the type checker already runs your code, there's not much left for a macro to do.

Other sides of the same question

  1. What does the wrapper promise when a handler returns?

    Neat abbreviation. I'd still like to see what it's agreeing to. The wrapper macro installs a handler around a parser, and the…