@trusted in its current form needs to go. Its design is badly broken, as it leaks implementation details and encourages writing unsafe code.

The Problem
———————————

First, there is no point in having @trusted in the function signature. Why? From the perspective of the caller of the function in question, @safe and @trusted mean exactly the same thing. If you are not convinced about that, just consider that you can wrap any @trusted function into a @safe function to make it @safe, and vice versa.

So the current situation is similar to having two keywords `pure` and `pure2` in the language, which are completely equivalent for the consumer of an API. This is in itself a problem, since it is a pitfall for writing generic code. To stick with the example, it's easy to check only for `pure` in a template constraint when you really meant to accept both `pure` and `pure2` – and you _always_ want to accept both.

But is this alone enough to warrant a change to the language at this point? Probably not. But besides that, the current design also leads to problems for the implementation side:

One issue is that the distinction unnecessarily restricts the implementation in terms of interface stability. Yes, @safe and @trusted are equivalent from the caller's perspective, but they are mangled differently. This means that changing a function signature from one to the other is a breaking change to the ABI, and as the mangled name is available in the program (which is e.g. what std.traits.FunctionAttributes), also to the API.

Thus, you can't just change @trusted to @safe or vice versa on the implementation side if you make changes code which require @trusted, resp. cause it to be no longer needed. Sure, you can always move the implementation into a new, properly marked function, and make the original function just a wrapper around it. But this is kludgy at best, and might be unacceptable in the case of @safe -> @trusted for performance optimizations, if the inliner doesn't kick in. So, the only reasonable choice if you want to provide a stable interface in this case is to mark all functions which could ever possibly need to access unsafe code as @trusted, forgoing all the benefits of automatic safety checking.

But the much bigger problem is that @trusted doesn't play well with template attribute inference and makes it much too easy to accidentally mark a function as safe to call if it really isn't. Both things are a consequence of the fact that it can be applied at the function level only; there is no way to apply it selectively to only a part of the function.

As an example how this is problematic, consider that you are writing a function which takes some generic input data, and needs to do (unsafe) low-level buffer handling internally to efficiently do its job. You come up with a first implementation, maybe only accepting arrays for the sake of getting it working quickly, and add @trusted as your dirty buffer magic isn't visible from the outside, but does break attribute inference. Later, you decide that there is no reason not to take other range types as input. Fortunately, the actual implementation doesn't require any changes, so you just modify the template constraint as needed, and you are good. Well, no – you've just completely broken all safety guarantees for every program which calls your function, because empty/front/popFront of the passed range might be @system.

Now, you might argue that this is a contrived scenario. Yes, the mistake could have easily be avoided, @trusted on a template declaration should always raise a red flag. But cases like this _do_ occur in real-world code, and are easy to miss: The recently added std.uuid originally had a similar bug, which went unnoticed until during the vote [1] – at that point, a number of people, mostly experienced contributors, had reviewed the code. A safety system which is easy to break by accident is somewhat of a futile exercise.

Can you correctly implement such a template function with today's @trusted? Yes, there are workarounds, but it's not quite easy. One way is to explicitly detect the @safe-ty of the code accessed via template arguments and switch function prototypes using static ifs and string mixins to avoid code duplication. For an example of this, see Jonathan's new std.range.RefRange [2]. It works, but it isn't pretty. The average programmer will, just as done in the revised version of std.uuid [3], likely give up and accept the fact that the function isn't callable from safe code. Which is a pity, as we should really utilize the unique asset we got in SafeD to the fullest, but this only works if everything that could be @safe is marked as such. The situation won't get better as we continue to advocate the use of ranges, either.

To summarize, there are, at least as far as I can see, no advantages in distinguishing between @safe and @trusted in function signatures, and the function-level granularity of @trusted yields to avoidable bugs in real-world code. Fortunately, we should be able to resolve both of these issues fairly easily, as described below.


A Solution
——————————

Let me make something clear first: I am _not_ intending to remove @trusted from the language. As a bridge between the @safe and @system worlds, it is an integral part of SafeD. What I'm proposing is:

1) Remove the distinction between @safe and @trusted at the interface (ABI, API) level. This implies changing the name mangling of @trusted to Nf, and consequently removing the distinction in DMD altogether (at least in user-facing parts like .stringof and error messages). In theory, this is a breaking change, but as any code that doesn't treat them the same is buggy anyway, it shouldn't be in practice. As for std.traits.FunctionAttribute, we could either make trusted an alias for safe, or just remove documentation for the former and keep it around for some time (there is no way to deprecate an enum member).

2) The first step is necessary, but mainly of cosmetic nature (think `pure`, `pure2`). We still need to address for the granularity and attribute inference problem. The obvious solution is to add a "@trusted" declaration/block, which would allow unsafe code in a certain region. Putting @trusted in the function header would still be allowed for backwards compatibility (but discouraged), and would have the same effect as marking the function @safe and wrapping its whole body in a @trusted block. It could e.g. look something like this (the @ prefix definitely looks weird, but I didn't want to introduce a new keyword):

---
 void foo(T)(T t) {
   t.doSomething();
   @trusted {
     // Do something dirty.
   }
   t.doSomethingElse();
   @trusted phobosFunctionWhichHasNotBeenMarkedSafeYet();
 }
---

This is similar to other constructs we have today, for example debug {}, which allows impure code. It can be debated whether a block »argument« should introduce a new scope or not (like static if). The latter currently seems much more attractive to me, but I suppose it could be confusing for some.

In any case, while there is probably quite a bit of bikeshedding to be done for 2), I don't think there is much controversy about 1). So, let's try to get this done shortly after the 2.060 release – as discussed above, it is very unlikely that the change will break something, but the odds still increase over time. Also, there is currently a Phobos pull request [4] which will be influenced by the outcome of this discussion.

David



[1] http://forum.dlang.org/thread/[email protected]?page=2#post-pcaaoymspzelodvmnbvc:40forum.dlang.org [2] https://github.com/D-Programming-Language/phobos/blob/c005f334ec34d3b0295d5dbf48212972e17823d0/std/range.d#L7327 [3] https://github.com/D-Programming-Language/phobos/blob/c005f334ec34d3b0295d5dbf48212972e17823d0/std/uuid.d#L1193
[4] https://github.com/D-Programming-Language/phobos/pull/675

Reply via email to