Compile-time reflection

Kirk McDonald kirklin.mcdonald at gmail.com
Sun Jul 1 15:59:40 PDT 2007


The subject of compile-time reflection has been an important one to me. 
I have been musing on it since about the time I started writing Pyd. 
Here is the current state of my thoughts on the matter.

----
Functions
----

When talking about functions, a given symbol may refer to multiple 
functions:

void foo() {}
void foo(int i) {}
void foo(int i, int j, int k=20) {}

The first thing a compile-time reflection mechanism needs is a way to, 
given a symbol, derive a tuple of the signatures of the function 
overloads. There is no immediately obvious syntax for this.

The is() expression has so far been the catch-all location for many of 
D's reflection capabilities. However, is() operates on types, not 
arbitrary symbols.

A property is more promising. Re-using the .tupleof property is one idea:

foo.tupleof => Tuple!(void function(), void function(int), void 
function(int, int, int))

However, I am not sure how plausible it is to have a property on a 
symbol like this. Another alternative is to have some keyword act as a 
function (as typeof and typeid do, for instance). I propose adding 
"tupleof" as an actual keyword:

tupleof(foo) => Tuple!(void function(), void function(int), void 
function(int, int, int))

I will be using this syntax throughout the rest of this post. For the 
sake of consistency, tupleof(Foo) should do what Foo.tupleof does now.

To umabiguously refer to a specific overload of a function, two pieces 
of information are required: The function's symbol, and the signature of 
the overload. When doing compile-time reflection, one is typically 
working with one specific overload at a time. While a function pointer 
does refer to one specific overload, it is important to note that 
function pointers are not compile-time entities! Therefore, the 
following idiom is common:

template UseFunction(alias func, func_t) {}

That is, any given template that does something with a function requires 
both the function's symbol and the signature of the particular overload 
to operate on to be useful.

It should be clear, then, that automatically deriving the overloads of a 
given function is very important. Another piece of information that is 
useful is whether a given function has default arguments, and how many. 
The tupleof() syntax can be re-used for this:

tupleof(foo, void function(int, int, int)) => Tuple!(void function(int, 
int))

Here, we pass tupleof() the symbol of a function, and the signature of a 
particular overload of that function. The result is a tuple of the 
various signatures it is valid to call the overload with, ignoring the 
/actual/ signature of the function. The most useful piece of information 
here is the /number/ of elements in the tuple, which will be equal to 
the number of default arguments supported by the overload.

One might be tempted to place these additional function signatures in 
the original tuple derived by tupleof(foo). However, this is not 
desirable. Consider: We can say any of the following:

void function() fn1 = &foo;
void function(int) fn2 = &foo;
void function(int, int, int) fn3 = &foo;

But we /cannot/ say this:

void function(int, int) fn4 = &foo; // ERROR!

A given function-symbol therefore has two sets of function signatures 
associated with it: The actual signatures of the functions, and the 
additional signatures it may be called with due to default arguments. 
These two sets are not equal in status, and should not be treated as such.

----
Member functions
----

Here is where things get really complicated.

class A {
     void bar() {}
     void bar(int i) {}
     void bar(int i, int j, int k=20) {}

     void baz(real r) {}

     static void foobar() {}
     final void foobaz() {}
}

class B : A {
     void foo() {}
     override void baz(real r) {}
}

D does not really have pointers to member functions. It is possible to 
fake them with some delegate trickery. In particular, there is no way to 
directly call an alias of a member function. This is important, as I 
will get to later.

The first mechanism needed is a way to get all of the member functions 
of a class. I suggest the addition of a .methodsof class property, which 
will derive a tuple of aliases of the class's member functions.

A.methodsof => Tuple!(A.bar, A.baz, A.foobar, A.foobaz)
B.methodsof => Tuple!(A.bar, A.foobar, A.foobaz, B.foo, B.baz)

The order of the members in this tuple is not important. Inherited 
member functions are included, as well. Note that these are tuples of 
symbol aliases! Since these are function symbols, all of the mechanisms 
suggested earlier for regular function symbols should still work!

tupleof(A.bar) => Tuple!(void function(), void function(int), void 
function(int, int, int))

And so forth.

There are three kinds of member functions: virtual, static, and final. 
The next important mechanism that is needed is a way to distinguish 
these from each other. An important rule of function overloading works 
in our favor, here: A given function symbol can only refer to functions 
which are all virtual, all static, or all final. Therefore, this should 
be considered a property of the symbol, as opposed to one of the 
function itself.

The actual syntax for this mechanism needs to be determined. D has 
'static' and 'final' keywords, but no 'virtual' keyword. Additionally, 
the 'static' keyword has been overloaded with many meanings, and I 
hesitate suggesting we add another. Nonetheless, I do.

static(A.bar == static) == false
static(A.bar == final) == false
static(A.bar == virtual) == true

The syntax is derived from that of the is() expression. The grammar 
would look something like this:

StaticExpression:
	static ( Symbol == SymbolSpecialization )

SymbolSpecialization:
	static
	final
	virtual

Here, 'virtual' is a context-sensitive keyword, not unlike the 'exit' in 
'scope(exit)'. If the Symbol is not a member function, it is an error.

A hole presents itself in this scheme. We can get all of the function 
symbols of a class's member functions. From these, we can get the 
signatures of their overloads. From /these/, can get get pointers to the 
member functions, do some delegate trickery, and actually call them. 
This is all well and good.

But there is a problem when a method has default arguments. As explained 
earlier, we can't do this:

// Error! None of the overloads match!
void function(int, int) member_func = &A.bar;

Even though we can say:

A a = new A;
a.bar(1, 2);

The simplest solution is to introduce some way to call an alias of a 
method directly. There are a few options. My favorite is to take a cue 
from Python, and allow the following:

alias A.bar fn;
A a = new A;
fn(a, 1, 2);

That is, allow the user to explicitly call the method with the instance 
as the first parameter. This should be allowed generally, as in:

A.bar(a);
A.baz(a, 5.5);

Given these mechanisms, combined with the existing mechanisms to derive 
the return type and parameter type tuple from a function type, D's 
compile-time reflection capabilities would be vastly more powerful.

-- 
Kirk McDonald
http://kirkmcdonald.blogspot.com
Pyd: Connecting D and Python
http://pyd.dsource.org



More information about the Digitalmars-d mailing list