I came up with my own way of dealing with this. I'm not sure if it's the simplest way. I was careful to make sure I dealt with all possible namespace issues, as well as possible recursive calls where code blocks need to be inserted within code blocks. My method also makes sure to avoid evaluating the arguments of any functions until the full code has been assembled.
I define two functions intended for the end user to use directly, Meta[]
and Macro[]
. In addition to these are a few helper functions, and some specialized versions of Hold[]
(used so that they can be found in patterns, without conflicting with any literal Hold[]
s the user may have typed in the code in the first place!).
(* Macro is designed to access the source code of a Function body and return it
wrapped in Hold, with the appropriate variables plugged in *)
SetAttributes[Macro,HoldAll];
(* One trivial definition so that the blue highlighting goes away *)
Identity[Macro[any___]]^:=Macro[any];
(* We create a specialized version of Hold for dealing with Macros, so that we don't
erroneously release other Holds that the programmer might have
actually intended to stay! *)
SetAttributes[HoldMacro,HoldAll];
Identity[HoldMacro[any___]]^:=HoldMacro[any];
(* Various specialized Hold functions for destructuring a function call and
reassembling it, without prematurely evaluating its arguments
(just in case an argument is a global variable that has been localized!) *)
SetAttributes[HoldArg,HoldAll];
Identity[HoldArg[any___]]^:=HoldArg[any];
(* We leave Macro inert and instead define a function that accesses the contents.
Notice that the Macro head already has the HoldAll attribute;
thus getMacroSource does not need HoldAll. In fact, we *want* it to evaluate! *)
getMacroSource[Macro[fun_[args___]]]:=
With[{def=OwnValues[fun][[1,2]],heldargs=ReleaseHold@(HoldArg/@Hold[args])},
If[Head[def]===Function,
Replace[MapAt[HoldMacro,def,{2}][heldargs],HoldArg[arg_]:>arg,Infinity]
]];
(* Default case *)
getMacroSource[expr:Except[_Macro]]:=expr;
(* Replace any macros in a block of code with their source code, wrapped in HoldMacro *)
SetAttributes[injectMacros,HoldAll];
injectMacros[expr_]:=
(* search for macros in reverse order of depth *)
With[{positions=Reverse@SortBy[Position[expr,HoldPattern@Macro[_]],Length]},
If[Length[positions]>0,
(* Traverse the list of macro positions *)
Fold[
With[{source=getMacroSource@Extract[#1,#2]},
(* Recurse through the macro source to look for more macros *)
(* Always inserts the deepest nested macros first! *)
ReplacePart[#1,injectMacros[source],#2]]&,
expr,
positions],
(* else *)
(* Make sure there is an exit from recursion *)
expr]
]
(* Meta can be used with any expression, but especially as Meta@Compile[...].
Meta keeps its argument wrapped in Hold, looks for any unexpanded Macro heads,
and expands them to their source code before evaluating the entire expression *)
SetAttributes[Meta,HoldFirst];
(* Expand Macro heads to source code wrapped in Hold with appropriate values plugged in;
then remove the inner Hold heads to inject the source code.
Finally remove the outer Hold *)
Meta[expr_]:=
ReleaseHold@
Replace[
injectMacros[Hold[expr]],
{HoldMacro[any___]:>any},
Infinity]
The usage is as follows:
Needs["CompiledFunctionTools`"];
bar = Function[{x, y}, x + y];
foo = Meta@Compile[{{x, _Integer}, {y, _Integer}},
x*y*Macro@bar[x, x] + Macro@bar[y, x]];
Notice that bar
is defined as an ordinary function; its usage as a macro in the source code for foo
happens at the time you write the code for foo
. We can see the source code that Compile[]
actually sees:
Extract[foo, {{-2}}]
(* {Function[{x, y}, x y (x + x) + (y + x)]} *)
which clearly contains no function calls to bar[]
, but has completely inlined the code. bar[]
can also contain scoping constructs like Module
and they will behave correctly. bar[]
can also contain additional macros, and their code will also be expanded.
I'm not sure if this is the simplest way to achieve the desired result, as I see one of the solutions above just uses Rules directly. But I think I remember running into subtle problems with scoping constructs and function arguments prematurely evaluating. The objective here is to make sure everything is held until it has been put in place, so that Compile[]
sees it literally as though it had been typed all at once.