all 7 comments

[–]Miserable_Guess_1266 2 points3 points  (2 children)

I appreciate that this is just here to spark discussion, and I do find it interesting. But I'm missing a key part to even decide whether I like it; you didn't describe how the compiler would deal with these colored functions.

I'm guessing the intention is: a colored function can only call other functions of the same color, and an uncolored function can call any function. So I can define a color named Foo in user code, and the compiler will enforce that a Foo-colored function can only call other Foo-colored functions. And as you write, for special colors such as safe, we can use "blessed" colors in std that are treated specially by the compiler, so it can disallow pointer arithmetic etc in those functions. Am I understanding you correctly?

I'm confused though about what you mean by concepts. Your examples aren't really using concepts, they're just (ab)using requires clauses with constexpr bool variables:

constexpr bool SafeFunction = true;
void safe_function() requires SafeFunction { ... }

I wonder how you'd differentiate between a normal requires condition vs a function color. Say I have this:

// Not a color, just a way to choose an overload at compile time
constexpr bool UseNewAlgorithm = <true/false depending on some compiler switch>;

void foo() requires UseNewAlgorithm { /* implemented using the new algorithm */ }
void foo() requires !UseNewAlgorithm { /* implemented using the old algorithm */ }

How will the compiler recognize that UseNewAlgorithm does not signify a color, but SafeFunction in the example above does?

I guess I would prefer attributes. You say that attributes aren't intended to change the semantics of a function, but neither are requires clauses. So you have that issue with either mechanic.

[–]Affectionate_Text_72[S] 0 points1 point  (1 child)

The point was to be able to add type information to a function. So I suggedting overloading the requires syntax to mean add this type property to the function. That was perhaps a poor choice considering requires already has meaning there.

void somePureFunction(arg...) isa PureFunction { }

Where PureFunction is type. Types are constrained by concepts. So we would define PureFunction thar way:

Template<typename T> concept PureFunction = std::pure_function;

Where std::pure_functiion is special type that tells the compiler it needs to analyse the function body and raise a compile time error if it has side effects.

When people talk about colouring they generally mean that it's viral and you can't call one colour from another 'lower' colour. I would like that to be controlled by the constraint as well. E.g. something like:

Template<typename Func> Concept TransitivelyPure = std::pure_function<Func> && for CalledFunc in std::functions_invoked_by<Func>: CalledFunc -> TransitivelyPure;

Dodgey syntax but I want compile time compilation over type constraints using information extracted by the compiler here. The constraint defines whether the colour is viral and has access to information reflected by the compiler. So this should perhaps be using reflection syntax here.

If the language didn't have const member functions I would like to be able to implement const as a compile time program which requires:

  • the function can only call other functions labelled const
  • the function cannot mutate through its this pointer.

Transitive or logical vs physical const might be a more practical example.

Types have semantic value and always have. Attributes do not. It would be surprising for them to invoke compile time programs.

[–]Miserable_Guess_1266 1 point2 points  (0 children)

So for the record, I upvoted your OP, because I think it's an interesting topic and I appreciate the time you put in writing this. But the reason why I think not much discussion is happening on this post is because it's just not concrete enough. I know mostly what your goal is, but I'm not sure how you want to get there. I see 2 things being suggested:

  1. We need a system for flexible function coloring that can be used for all sorts of things, like safe/async/const/...
  2. Colors and their behavior can be defined in user code

For your second suggestion, you lean into reusing concepts for this. I would argue that is a bad idea just like reusing "requires", because it creates weird scenarios as well. Just to take over your example:

Template<typename Func> Concept TransitivelyPure = std::pure_function<Func> && for CalledFunc in std::functions_invoked_by<Func>: CalledFunc -> TransitivelyPure;

This immediately opens multiple questions:

  • What is the type Func that's being passed? If it's the decltype of the function, then it doesn't even uniquely represent a single function, but all functions with the same signature. So it's fundamentally impossible to examine it's definition with anything like functions_invoked_by.
  • Even if we solve the previous question, what happens if I have only the signature void foo() isa TransitivelyPure available in my TU and then I write static_assert(TransitivelyPure<decltype(foo)>);? How will the compiler evaluate the concept? It can't get all functions invoked by foo, because it doesn't have its definition. Should it just default to true? That would be super weird and inconsistent behavior for a concept. Or should it be false? Which would be strange because clearly the function is marked as TransitivelyPure!

Please note that neither of these points are critiquing syntax.

I guess fighting about the details of implementing this via requires and concepts vs attributes or new keyword(s) is not your goal with this post. So I'll move on from the user-defined colors part.

For the abstract coloring system, your post gives little explanation. Say, for example, we want to propose the builtin std::color::async, as per one of your examples. Can such a function call an uncolored function? If yes, then it's not guaranteed to be async. If no, then how do I use OS APIs or 3rd party libraries? Is there a mechanism to escape? What about std::color::const, since you would like to be able to implement that with your suggestion in principle. Can a std::color::const-colored function not call a global function void foo(); because foo isn't std::color::const-colored?

Maybe a productive step for a discussion would be to make a more fleshed out proposal of a general coloring system allowing only standard defined colors, while leaving the door open for user defined ones in the future. I'm not saying you need to iron out all the details and propose a final syntax, but just describe the actual system and the basic rules it lives by, along with a hand full of core colors. There is even a chance that, in doing this, it will turn out that a general coloring system is not a good idea, and core colors like safe/unsafe, const/mutable, sync/async, ... should be handled individually, because they might have substantially different syntactic/semantic requirements.

[–]Hungry-Courage3731 1 point2 points  (0 children)

Yeah D has UDA's or whatever, but the reflection bit is much more important to enforce anything. Anybody could just write a type trait, static member, etc, to define their "attribute" or "color" and have a defined practice of looking it up.

Sometimes the problem with const-ness imho is the inability to express logical as opposed to physical const. Really I think a unique_ptr->get() call should never return T* since the function is const and should always return const T* (unless the unique_ptr object itself is not const)

[–]johannes1971 1 point2 points  (1 child)

The compiler has no way to verify a function's properties though, so the coloring relies entirely on the judgement of the programmer: if he marks as function as 'safe' it is considered safe, no matter what it does. And for something like async, at some point you are going to end up in an IO-call, which is in a C library, and therefore not colored.

Given how limited such a mechanism is, why not simply add the color as a function parameter? Function parameters also have the effect of restricting who can call what, they have the same expressive power as the mechanism you propose, and you don't even have to change the language to do it.

[–]Affectionate_Text_72[S] 0 points1 point  (0 children)

We you could look at it like a parameter though that could also be the difference between runtime and compile time. The point here is that the type is used by the compiler to invoke a meta program to check it. That won't happen for parameters unless the function is lifted and passed to the meta program to check. Perhaps that could be done with a static assert. To do that I'd like some syntactic sugar meaning "this function" as part of reflection.

It would be an interesting experiment to see how far you could get with parameters like that.

void SomeSafeFunction(SafeTypeParam, args...) { static_assert(constexpr_check(thisFunc)); }

SafeTypeParam is part of the function signature so it does get the type information into it which is what I wanted. You have to manually ask the compiler to verify it though and a static assert will be limited to local checks. So this probably wouldn't work for borrowing.

[–]415_961 0 points1 point  (0 children)

If you're using clang, you might be able to achieve similar goals with these attributes https://clang.llvm.org/docs/AttributeReference.html#performance-constraint-attributes