Skip to content

Instantly share code, notes, and snippets.

@rikkimax
Last active April 6, 2021 06:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rikkimax/826e1c4deb531e8dd993815bf914acea to your computer and use it in GitHub Desktop.
Save rikkimax/826e1c4deb531e8dd993815bf914acea to your computer and use it in GitHub Desktop.

My vision for D in the near future

As of this writing of this article D is on the verge of getting memory safety mechanisms that prevent memory escaping and use after free. Which great for D but it can surpass other languages in the C family both in terms of abstraction and also memory safety.

Step 1. An extension on the borrow checker

Walter Bright is currently in the process of developing a DIP for a borrow checker. This was spawned on by the success of Rust and its borrow checker. But unfortunately in terms of syntax this is a kind of magic syntax with different semantics being applied to a pointer even if the same syntax is being used within a function.

We I think can do better than applying different semantics to a pointer magically.

Instead of an automatic detection and determination lets introduce a new storage class called headconst. This is fairly simple in behavior that you may have already guessed. It is a memory reference that cannot be altered in anyway nor implicitly casted to anything else (as to prevent free'ing or corrupting the memory).

The effect of this new storage class is of course transitive just like const is today. To prevent any internals being linked by pointer.

As a reference it is either a slice or a pointer. The protections are from the compiler, and are not reflected within the codegen itself.

Of course since headconst is tied to the lifetime of the memory owner, it will rely on the already existing work of DIP25, DIP1000 and of course the work that Walter has already done for his borrow checker implementation.

I propose a head-const implementation over the borrow checker for a single but important reason. From my recollections I do not remember anyone asking for a borrow checker but countless of others asking for head-const in D. This solution has multiple use cases on top of memory safety that will satisfy even the most uncaring for memory safety to provide it.

An example of this can be found here: https://gist.github.com/rikkimax/4cb2cc8ddcac33c1a9bb20de432f9dea

Step 2. Named members via template parameters

This proposal adds the ability to specify types and values which are members of a template via a template parameter. It is an extension of a named arguments DIP such as Walter's and was included in my DIP 1020.

There are a few variants of syntax, but they all do the same thing. A few of these are:

alias X = X;
enum Y = Y;
@property {
    typeof(auto) SomeType;
    auto SomeValue;
}

and

template Foo(public SomeType, public int SomeValue) {
}

Which can be used like:

static assert(Foo!(SomeType: int, SomeValue: 0).SomeType == int);

Draft DIP pre template: https://gist.github.com/rikkimax/046fb4451e8cbac354ecb292f9a76798#file-template-parameters-as-properties-of-a-type-md

Signatures

Now you may be thinking why did this get a heading and not be a step? Well it is simple, this section will be devided up into two further sections.

First is type classes, this provides a static interface which can be used to compare against other types. This is the set of features most people who know signatures (type classes, Rust trait's and Swift protocols) will expect.

The second set is the dynamic bits which give signatures their runtime representation. Programmers from C and related languages will recognize a few design decisions due to having written code like it.

Step 3. Type verification

We need a way to tell the compiler directly that we want to compare another type against a specification and what defines that specification.

There will always be situations where we can't do that directly so instead we provide ways to inform the compiler what those conditions are and why it did or probably did fail. This can be done by using static assert with its message parameter for why it failed. These errors can be eaten by the compiler and fail any type checks being done implicitly. Due to being eaten, they can be propergated up should the need arise in the calling code.

For type verification purposes a signature has three items in its body of interest. First is the ability previously discussed to say some property of the implementation is wrong. The second is named members settable via template parameters but is also able to be inferred from an implementation. Lastly is method declarations.

A method declaration is the same as a method declared in a interface. It has no body unless its final.

A signature is able to inherit from other signatures too. Unlike with a class system there is no problem of a diamond inheritance. Signatures work based upon less restrictive in the interface to more restrictive in the implementation. In other words a child of another signature may make the method more specialized (i.e. adding const, child class as a return type ext.). But it may not remove existing specializations.

An example of this is:

signature InputRange {
    @property {
        typeof(auto) ElementType;
    }
    
    ElementType front();
    bool empty();
    void popFront();
}

signature ForwardRange : InputRange {
    ForwardRange!(ElementType: ElementType) save();
}

You can of course compare a signature instance and a signature declaration via the is expression with awareness of happening inside another signature of the same declaration (false if true). With : checking for the non-template-initiated type and == checking the exactness (with or without template initialization) exactness. This same method is used for comparing a signature (either the declaration or the template inititialized version) with a struct or class.

Existing template parameter syntax is extended to match existing = and : support as per the prior statement of is expression.

Step 4. Virtual representation

Some type that can construct a Signature instance:

void func(auto:Signature arg) => void func(T)(T:Signature arg)

Verify and document return type of a function:

auto:Signature func() =>

auto func() { ... }
static assert(is(ReturnType!func : Signature));

Signature as a function parameter without being template initialized:

void func(Signature value) => void func(T:Signature)(T value)

ABI:

struct SignatureInstance(ParamsExt) {
    copy constructor
    destructor

    void* ptr; // aka this
    ...fields
    ...methods
}

In other words: void*[Size].

Where field is a pointer and method is a function pointer (unless there are cases where it must be a delegate and have its own context pointer). Copy constructor and destructor are treated as if they were methods that are virtual.

A signature instance can be implicitly created when passed as a function argument or as a return value.

I.e.

Signature foo(Signature) {
    return ImplementationB();
}

...

foo(ImplementationA());

Within a signature this refers to the implementation and never the signature itself. typeof(this) is available when inferring and the expression (working upwards) should be replaced with false. So that:

signature Foo {
    static assert(!is(typeof(this)) || __traits(hasMember, typeof(this), "something"));
}

when inferring the second expression in the static assert is used and not the first.

Methods

A method that is final is described under type verification, otherwise it is virtual.

If an implementation has a method, then the signature does not need to provide a body for a method declaration. If a method body is provided and the implementation provides one, then the implementation is chosen unless override is used on the method declaration.

Step 5. Safe by default

Making safe be the default in D is a highly desirable feature that would create breakage in the short to medium term and will likely split the community in some way.

But there is a way to do it with minimal to no code breakage.

This is done by utilizing the head-const feature described in step 1.

A function is automatically @safe if it fulfills this rule: All pointer declarations (slices, classes, raw ext.) including parameters must be headconst or it must have an expression assigning it via new. If the later of the two of the rule it must never be assigned to.

To work around this you can mark a function @system or @trusted to get the old behavior.

Furthermore ref and out parameters on a function should be rewritten as headconst while keeping the same behavior from outside the function.

@rikkimax
Copy link
Author

rikkimax commented Mar 9, 2021 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment