For Better or Worse: INLINER (and MACRO)

So it's long been theorized that there could exist a form of function that would splice its result into the feed of execution. And now we have it:

 weirdness: inliner [x [block!] :refinement] [
      either refinement [
          'reverse
      ][
          spread compose [append (x) first]
      ]
 ]

 >> data: [a b c]

 >> weirdness data [1 2 3]
 == [a b c 1]  ; acts like you wrote `append [a b c] first [1 2 3]`

 >> data
 == [a b c]

 >> weirdness:refinement data [1 2 3]
 == [1 2 3]   ; acts like you wrote `reverse [a b c] [1 2 3]`

 >> data
 == [c b a]  

So you return either an element or a SPLICE! of what you want injected at the callsite. (Or a VOID to have same effect as an empty splice.) If you want things to be bound to things that the INLINER knew about (e.g. the argument X in the case above) then you have to bind it before you give it back as a splice.

NOTE: Here Be Dragons! :dragon:

The function interface for what the inliner produced above looks like it only takes one argument, but it winds up being effectively variadic. Here you see it deciding to take one or two.

Note that if you put an infix operator at the beginning of a macro splice, it will not be able to see its left. So if you want an inliner to see the left hand side parameter, the inliner itself has to be infix. It will see the infix parameter, but won't "consume" it:

 add1020: infix inliner [] [
     return [+ 1020]
 ]

 >> 304 add1020
 == 1324

As a general reminder of why you can't decide infix "after the fact"... the concept of a single instruction "step" is one that has a finishing point. If all functions reserved the right to be infix, then that would mean a EVAL of a BLOCK! couldn't be separated into individual evaluation steps...each function would run glued to the last. It would be possible to avoid this if COMMA!s between expressions were enforced so you called out where every expression ended, but we obviously do not want that!

3 Likes

In 2020 I called this "MACRO".

But here in 2025 I've retconned things to its new name as "INLINER". Because while it is powerful and general (allowing you to run arbitrary code to build the code you're injecting), it's a bit verbose for what we might think of as a conventional "macro"...needing a call to SPLICE for nearly all cases, despite splicing being what macros generally do.

So MACRO is now something that on the surface :man_rowing_boat: looks a lot simpler:

>> xy-plus-100: macro [y] [x * y + 1000]

>> x: 10

>> xy-plus 20
== 1030

But in reality, it's deeper than that. It allows you to control binding, to discern when things refer to the environment where the macro is defined -OR- when they're expected to refer to the environment where the macro is instantiated.

If you use $TIED things, the macro will assume you wanted them bound in the originating environment:

>> xplus: (let x: 10, macro [] [$x +]])

>> x: 20

>> xplus 1000
== 1010

Plain items will be left unbound (unless they are arguments to the macro).

>> xplus: (let x: 10, macro [] [x +])

>> x: 20

>> xplus 1000
== 1020

MACRO Currently Built Using INLINER

Eventually it could have an efficient native implementation. But so long as the dialect is being worked out, it's easier to maintain it as a usermode inliner.

Got ideas for cool features of MACRO? I am thinking that it might be that anything quoted drops one level and gets left as-is.

>> m: (let x: 10, macro [] ['$x])

>> x: 20

>> get m  ; acts like (get $x) and not (get '$x)
== 10

So if you literally want something quoted, you might double quote it.

>> m: (let x: 10, macro [] [''$x])

>> x: 20

>> m  ; acts like ('$x) and not (''$x)
== $x

Too weird to be a default? Maybe controlled with an option.

But the point is just that I thought INLINER was not "dialect-powered" enough to take the prized MACRO name.

We really want to take dialecting to the moon

:rocket: :first_quarter_moon_face:

2 Likes