Saturday 2 July 2011

N-Pass

I've been thinking more and more about complex compile-time operations. The fundamental problem is, where do you draw the line? If I allow metaprogramming, do I allow meta-meta-programming? How do I allow complex types- which might be metaprogrammed themselves- to be used in metaprogramming? For example, it's a valid and known optimization in std::copy for certain POD types to go to memcpy or memmove, something which I should always allow, which the three-pass system doesn't. But then, I'd have to introduce a new pass.

constexpr type array(type t, int i) {
    type t;
    t.add_public_member_array(t, i); // etc
}


constexpr void func(int i) {
     array(t, i) arr; // How do I deal with this?
}


So far, I've come up with two solutions, and the first is the three-pass system that I discussed previously. You would use SFINAE like now, etc. The downside of that is basically that I'd have to keep templates, which would be an incredible mess and massive constrast to the smooth, easy constexpr system that I have now.

The other solution is something I've termed N-pass. This is where the compiler performs a variable number of compiling passes to compile the program. The trouble is, more passes means a longer compile time, and determining how many passes are appropriate could be ... "fun". However, the end result would be much better- there would be effectively no blur between compile-time and run-time.

The language would only have the notion of "this pass", which is "runtime" in C++, and "the previous pass", which is "constexpr" in C++0x. Then, the compiler would resolve which passes are which.

// Takes no "previous pass" arguments, takes two "this pass" arguments
type array()(type t, int MaxSize) {
    type t;
    t.add_public_array(t, MaxSize, "data");
    // A lambda function- takes no "prev pass" arguments, 

    // two "this pass" arguments "this" and "index"
    t.add_public_member_function("at", [&]()(t& ptr, int index) {
        if (index >= MaxSize)
            throw std::runtime_error("Array access out of range.");
        return ptr.data[index];
    });
}

float GetSomeShiz(int x)(array(float, x) arr, int i) { 

    // OK- x is a prev-pass argument
    return arr.at(i);
}
float GetSomeShiz()(int i, array(float, i) arr) { 

    // BAD, i is a this-pass argument
}


A slightly more complex example:


void copy(type t)(t* dest, t* source, int size) {
    if (t.IsPod()) {
        memcpy(dest, source, size);
    } else {
        for(int i = 0; i < size; i++) {
            dest[i] = source[i];
        }
    }
}


The trouble with this is that it's not immediately clear which statements are executed at which pass. Obviously anything depending on the "this-pass" arguments must be executed at this pass. However, some statements like the loop (if the size were a "prev-pass" argument) might be ambiguous. Is that good, or bad?

The only upside of this system is that functionality can be compiled at "this pass" and then kept that way, regardless of how many actual passes it ends up being used in. Tell me what you think.

Another advantage I've come up with (maybe) is some surprising overloading possibilities, in addition to what I already had with named variables.

// If the size is a "prev-pass" variable, execute some magic optimized version
void copy(type t)(t* dest, t* source)(int size) {
    if (t.IsPod()) {
        memcpy(dest, source, size);
    } else {
        for(int i = 0; i < size; i++) {
            dest[i] = source[i];
        }
    }
}

Another thing I've been considering is a quick syntax for what happens when. Like,


// If the size is a "prev-pass" variable, execute some magic 
// optimized version
void copy(type t)(t* dest, t* source, int size) {
    constexpr {
        if (t.IsPod()) {
            runtime {     
                memcpy(dest, source, size);
            }
        } else {
            runtime {
                for(int i = 0; i < size; i++) {                   
                    dest[i] = source[i];
                }    
            }        
        }
    }
}


As you can see though, it could get quite cluttered- especially if you wanted most than a couple levels nesting.

No comments:

Post a Comment