Saturday, 20 August 2011

Attributes- again

I've been thinking more about attributes. Fundamentally, some of them, like const, are views on a type, and some are copies. This is easily demonstrated because, quite simply, some of them are binary compatible and some aren't. That is, if you do a const_cast, then that's OK. But, if I had a Debug attribute, then the contents of a Debug std::vector may well be very different to the contents of a non-Debug std::vector, and something like a debug_cast could never exist, because there's no rules at all about what you might choose to change in a Debug std::vector. They may, in fact, be completely different types with little in common- or nearly everything in common.

An excellent example of this would be arrays. I might decide that non-Debug arrays don't check their boundaries but Debug arrays do, infact, check their boundaries. I might decide that instead of allocating Debug arrays on the native stack, I might allocate them in a separate memory arena so that they can never overflow the stack, and when an error happens, we can always get a stack trace. Hell, I might have "device" as an attribute and allocate a "device" array on the GPU or something similar- and who knows what that might involve?

That is, some attributes are like filters- you can cast them one way or another without breaking at the binary level, like const. However, some attributes are like template specialization- wholly unrelated to the source, at least, from a binary level.

This returns us to the core of the problem. What about primitive types? Users also have the option to lock user-defined types for the same effect. It's obviously illegal for user code to modify them- and most Standard types would do this, too. But I'd still want the choice to create a Tested Standard type if I wanted to.

This leads us to the inescapable conclusion that filters which do not add functionality must be valid on locked types. Furthermore, it also leads us to the next conclusion, which is that we can generalize const further to a simple removal filter.

namespace Standard {
    namespace Attributes {
        type Const {
            Type := Standard.Attributes.NonAddingFilter;
            Remove(TypeReference T) {
                // etc
            }
        }
        type Threadsafe {
            Type := Standard.Attributes.AddingFilter;
            Add(TypeReference T) {
                // etc
             }
        }
    }
}

The rules for casting are simple- the language will always implicitly reduce the functions available to the user, and always require an explicit cast to increase them. That is, NonAddingFilters are trivial to add, explicit to remove, and AddingFilters are trivial to remove, explicit cast to add, and Replacements cannot be cast at all. This means that AttributeCast will serve as a clean replacement for const_cast across any filter attributes.

For example, consider writing a webpage. You might decide logically, that an Unvalidated string would contain user input. In this case, the difference between a string and an Unvalidated string is, effectively, nothing- only that a function which writes output to the page will only take a string. The problem is that you want to have an implicit conversion in which you validate the contents. However, this violates the contract of a NonAddingFilter, namely that it doesn't add anything. You don't want to inherit from the String class, because that would produce problems like no virtual destructor, and you really don't want the implicit conversion. This is the ideal place for a Copy attribute. Copy attributes receive a type which has one member variable- the type being copied- and all functions (including constructors) appropriately forwarded. This is achievable in "DeadMG++" and not C++ because in "DeadMG++" you can go back and remove them if you don't want them, or simply start afresh. Then, all you would have to do is add a conversion operator and you're done.

namespace Web {
    type Unvalidated {
        type := Standard.Attributes.Copy;
        Result(TypeReference T) {
            T.Conversions.Add([]() {
                return Validate(this.Internal);
            });
        }
    }
}

Whilst writing up this example, I noted that if the function name is always a string then you can't access conversion operators- since the type might not even have an accessible string. This would be akin to executing arbitrary strings at compile-time. Therefore, the only way to allow a conversion to say, a compile-time type argument, would be to simply decltype() the function. This *also* yields the issue of adding functions which take compile-time arguments. If the type has an existing compile-time component, it would be inaccessible to them- that is, the only way to get compile-time functionality out of a type is to write it as a literal. I'm not too happy about this. I think it's going to be a fundamental flaw of the N-pass system that I have designed.

Secondly, the lvalue/rvalue deal. I think that the logical option is to split rvalue references and perfect forwarding. I will have one type, "Reference", which is almost like a "base class" of references. Const will not receive special treatment. A Reference may be either an lvalue or rvalue reference.

Another topic that deserves consideration is the rules for template deduction. Consider:

template<typename T> void func(T& t);
int main() {
    const int i = 1;
    func(i);
}

In C++ this will throw an error, as T cannot be deduced. In "DeadMG++", however, I am certainly considering allowing an equivalent snippet to compile. It's my understanding that C++ doesn't allow such things for legacy reasons- it would break old code. Since I don't have any old code, I feel no compunction to prevent such things from compiling. This is especially true as in "DeadMG++" I have arbitrary attributes- there would be no way to grab them all.

This still leaves me with the problem of volatile, though. The problem with volatile is not just that it behaves like a NonAddingFilter, which is fine, but it has significant implications for the code generating process- something not expressible within the attribute system.

Finally, I've realized that I've screwed myself just a little bit here- and that is, the order of functionality in a literal could matter, something I wanted to avoid. Inherently, the capability to mutate a type whilst it's still under construction is both dangerous and necessary. Consider a trivial snippet:

type T {
    SomeFunc(T) variable;
    SomeOtherFunc(T) othervariable;
}

Which of these two gains precedence? The function "SomeFunc(T)" could arbitrary mutate T in any fashion it desires. It could even lock the type. I could "lock" the type whilst it's constructing- that is, in the body of "T", then "T" is a const TypeReference, not a TypeReference.


I know that my blog posts are getting a bit infrequent and unreliable. I've got a lot going on right now and can't really afford the distraction of dreams about a better language. In a couple weeks, it'll all be over- probably for worse rather than better, but that's life.

No comments:

Post a Comment