First Draft: Static Single Assignment

Quirin Schroll qs.il.paperinik at gmail.com
Fri Jan 16 14:32:58 UTC 2026


On Wednesday, 3 December 2025 at 08:31:42 UTC, Walter Bright 
wrote:
> On 11/27/2025 11:41 AM, Jonathan M Davis wrote:
>> Okay, what's the real motivation here?
>
> The motivation is to find a way to express "single assignment".
>
> Single assignment has gained traction in programming circles, 
> as for long functions it can be hard to track if the value gets 
> reassigned in the function or not. By tagging with `final` the 
> compiler can enforce it to you.

That might be a fad. Something isn’t good because it’s popular. 
The value of a thing must be established independent of its 
popularity.

> I've been refactoring my code to use single assignment, as it 
> makes the code more readable.

That’s a style. A linter is the tool to enforce a style. A linter 
can be configured to ban things that a compiler shouldn’t ever 
ban.

In this case, a team might decide: When a function is more than 
25 lines of code long (counting all lines), then enforce single 
assignment, except for variables that are lint-annotated to be 
mutable. There can be project-specific exceptions that make sense.

It’s a much more flexible approach and does not burden the 
compiler and requires no thorough type system analysis. It 
doesn’t introduce core-language corner cases. Everyone knows 
linters are best-effort and won’t catch all problems.

>> If it's to prevent assignment, then
>> IMHO, that's what the DIP needs to say. It should not talk 
>> about preventing
>> "modification" or "mutation." If that's what you want, then 
>> just use `const`.
>> So, assignment needs to be called out as illegal rather than 
>> talking about
>> mutation or modification being illegal.
>
> `const` is different, as it is transitive.

Yes, and everyone knows `const` bans modification, not just 
assignment.

>> Of course, that also leaves the question of "op assignment"
>
> They're disallowed for `final` declarations.
>
>> ```d
>> final int i = 42;
>> i += 10;
>> ```
>> should be legal,
>
> I'm sorry, but the point is to make that illegal.

What about a class type?

For classes, there’s assignment of the handle and `opAssign`. 
They look identical in code, but they’re semantically very 
different. You could argue that class `opAssign` isn’t 
modification in the sense of `final` because it’s akin to 
assigning the pointed-to `int` of an `int*`.

>> should be illegal. And if the desire is to prevent operations 
>> like `+=` as
>> well, then const or immutable should be used. If that's not 
>> the case, then
>> we're going to have a very weird situation with user-defined 
>> types.
>
> Once a user defined type is constructed, if is final, then it 
> cannot be modified.

So, it can only call `const` member functions? At that point, it 
can be just `const`.

>> Of course, the other issue here is that because `final` is a 
>> storage class and
>> not a type qualifier, things are going to degrade to `const` 
>> pretty quickly
>> with pointers and references. For instance,
>> ```d
>> final int i = 42;
>> auto ptr = &i;
>> ```
>> is going to have to make `ptr` `const(int)*` or `const(int*)`
>
> or simply disallowed. I hadn't thought of that case, though 
> there are surely more cases I didn't think of!

At least at this point, you should stop and re-assess.

>> But to handle that cleanly in more complex cases, you'd really 
>> need `final` to
>> be a proper type qualifier, which would mean something like
>> ```d
>> final int i = 42;
>> final(int)* ptr = &i;
>> ```
>> since without that, it all degrades to const, significantly 
>> reducing the
>> utility of final in such code, but making final a type 
>> qualifier also opens
>> a very large can of worms which we probably don't want to open.
>
> I don't want to open that either, hence making it a storage 
> class. I understand the desire to make it part of the type 
> system, but I want to make it not part of the type system.

The problem is that you want a simple solution for something 
complicated and tradeoffy.

>> Of course, personally, I don't think that this feature adds 
>> enough value to
>> be worth adding at all, but it does feel very much like 
>> something being
>> welded onto the language to solve a corner case that doesn't 
>> work with const
>> rather than having a more holistic solution.
>
> I intend `final` to be optional and have it not have a 
> complicated set of rules for it. It is intended to be 
> lightweight.

Again, it will have weird semantics because you want a simple 
solution for something that’s actually complicated.

AFAICT, the yea-sayer see:

* A `final int` isn’t complicated, it’s just `const int`.
* A `final` reference type isn’t complicated, you can’t modify 
the reference.
* A `final T[]` isn’t too complicated, just don’t allow appending 
and reassignment.

Those can be handled with a template. I don’t know what’s up with 
`std.experimental.Final`, but it can definitely work for those. I 
tried implementing it in 2017 and realized: You need 
specializations for all sorts of types.

The nay-sayers see:

* All the above are useless.
* A `final` struct type with indirections is useless or 
complicated.

What follows is: If the solution mustn’t be complicated, it will 
be useless.

I tried fixing `std.experimental.Final` and realized for 
head-const, you absolutely have to make it a qualifier and a D 
template can’t substitute for that.

The idea is easy enough: `final` makes the first layer of 
indirection unmodifiable through this reference. That means you 
can’t let a `final` struct call mutable member function, but if 
the struct has indirections, `const` functions ask too much. You 
need dedicated `final` functions that only protect the first 
layer of the struct and allow for modification after that.

This is the bare minimum for `final` to work with structs and 
it’s the only place where it needs to be a core-language feature.

>> In any case, I really think that the DIP needs to be very 
>> clear about how
>> it's about assignment and _not_ about mutation, and the 
>> situation with the
>> compound assignment operators (which get overloaded with 
>> opOpAssign) needs
>> to be made very clear for both user-defined types and 
>> primitive types,
>> whereas the issue really isn't discussed at all with what's 
>> currently in the
>> DIP.
>
> Compound assignment operators would not be allowed on final 
> declarations. And it must be about preventing mutation, or it 
> is worthless.

Even then it’s very close to being worthless. I’ve given up on 
fixing `std.experimental.Final` back then for exactly that 
reason. It’s not worth it. It can’t work for structs, the only 
place something like it actually provides value.

The simple answer from the nay-sayers is: If you want to code in 
single assignment style, just do not re-assign. Just don’t do it. 
For most types, you can even use `const` to tie your hands. And 
if you really can’t trust yourself or your team members with 
something that simple, use a linter. After all, it’s 
syntactically very simple.

And I totally get why you like making it a storage class and not 
a qualifier: It makes the implementation easier, a lot easier. D 
already has 9 qualifications if you count all distinct 
combinations of qualifiers, and with `final` those become 13; 
you’d have to mangle it; it affects overloading (but it requires 
no new rules, since all relevant rules already exist for 
`const`); it necessitates a member function attribute to apply it 
to the implicit `this` parameter; probably more.

There’s another reason it’s not a great qualifier: It’s 
head-`const`. What about head-`immutable`? Head-`shared`? 
Head-`inout`?

I’m with Peter C on one aspect: `final` isn’t a good name.


More information about the dip.development mailing list