Skip to content

Conversation

@Blacksmoke16
Copy link
Member

Preview: https://github.com/crystal-lang/rfcs/blob/1195640eaf4f6780bb02e1839a35552adb42d04e/text/0018-macro-methods.md
PoC PR: TODO

A proposal based on the OP from crystal-lang/crystal#8835. Simple re-usable macro methods without the extra AST node monkey patching stuff.

@Blacksmoke16 Blacksmoke16 changed the title RFC: Macro Methods RFC 0018: Macro Methods Dec 23, 2025
@ysbaddaden
Copy link
Collaborator

Some personal observations:

  1. ProcLitral#call is the main alternative (and/or complement) and shall be documented here. See ProcLiteral#call for macros crystal#16451.

  2. Because of the proximity with def I think we should define macro defs on explicit types, for example macro def Foo.name and macro def self.name.

@BlobCodes
Copy link

  • I think a dedicated syntax would be beneficial to separate builtin intrinsic functions on TypeNode and user methods.

    Maybe something like $function() and $Type.function()?

  • Why should it only be possible to call macro defs within macros, and not other macros? Why are type restrictions only available on these new defs?

    The RFC differentiates macros and macro defs by stating that one returns an AST node and the other generates code, but the both are practically the same since you can always convert between the two.

    I think the following should be possible:

    macro fib(i : NumberLiteral)
      {% if i <= 1 %}
        {{ i }}
      {% else %}
        {{ fib(i-1) + fib(i-2) }}
      {% end %}
    end

    I think this would feel like a more holistic approach and allow existing libraries to make use of this feature more easily. These two functions would have exactly the same functionality:

    # This
    macro def test1(ARGS) : RET
      CODE
    end
    
    # Is always exactly the same as:
    macro test2(ARGS) : RET
      {{
        CODE
      }}
    end

    I think a very good source of inspiration would be the typst language. It's a very clean and modern TeX alternative and has this differentiation between "content mode" (code in square braces) and "code mode" (code in squiggly braces), but you can always switch back-and-forth between the two.

  • I think the ability to pass blocks to macro defs could substantially complicate the implementation, so I think it should be moved to future possibilities. It would also be a bit confusing since blocks in normal macros accept ASTNodes (and the keyword yield is already being used to "paste" this ASTNode).

I actually wanted to post an RFC like this soon, only adding type restrictions and the ability to call macros weithin macros, but leaving the macro def syntax as a future consideration. I think I'll now wait how this RFC progresses.

@straight-shoota
Copy link
Member

straight-shoota commented Dec 23, 2025

@BlobCodes that's an intriguing perspective.
Let's assume macro can be called from a macro expression and gets type restrictions (both seem feasible enhancements).
What's the difference between macro and macro def then? If it's just syntactic sugar for wrapping the entire body in {{ ... }}... 🤔

macro def format_name(name : StringLiteral) : StringLiteral
  name.underscore.upcase
end

macro format_name(name : StringLiteral) : StringLiteral
  {{ name.underscore.upcase }}
end

@Blacksmoke16
Copy link
Member Author

Blacksmoke16 commented Dec 23, 2025

  • Is the "return value" just whatever is the last line wrapped in {{ }}? What would the expected behavior be for a macro that is doing something like:
macro generate_constant(name)
  CONST_{{ name.id }}
end
  • Is the expectation that if you call one of these macros in a place its not expected to be, that it just fails or works but with undefined behavior?

  • If we want macros to support next and return keywords and such too would that conflict with using {{ }} as well?

  • Are we concerned at all that the API docs for these would just be mixed in with normal macros?

@BlobCodes
Copy link

Is the "return value" just whatever is the last line wrapped in {{ }}? What would the expected behavior be for a macro that is doing something like (...)

There is no explicit return value, the entire macro is executed as it works right now, its entire output is parsed into an ASTNode and this ASTNode is then returned to the caller.

If the given example macro was called with the name "foo", it would return Path("CONST_foo").
If the example macro was called with the name "A = 2", it would return Assign(@target = Path("CONST_A"), @value = NumberLiteral(2))

If we want macros to support next and return keywords and such too would that conflict with using {{ }} as well?

Yes, explicit returns would probably conflict with the current template syntax.

That's something that could be explored with macro defs.

Is the expectation that if you call one of these macros in a place its not expected to be, that it just fails or works but with undefined behavior?

Why should a macro result in undefined behaviour if it's called within a nested context?

@Blacksmoke16
Copy link
Member Author

Why should a macro result in undefined behaviour if it's called within a nested context?

I was mainly thinking about my use case where some of my macro defs would be like:

macro foo(x)
  {%
    # Interacting with constants and stuff
    # Intended to be called within a macro expression at certain places in the code
  %}
end

So if you tried to use this macro outside of a macro expression where you're expected for it to run, what would it do? Probably error that the const is unknown? Maybe not a big deal in practice, but this is kinda why i liked macro def being a separate thing. Just easier, for me at least, mentally to know that "okay these macros i use in a macro context while this other kind of macro is for this other context" versus needing to possibly handle the case where a macro is used in a place it wasn't expected to be.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants