November 12, 2021

On 11/12/21 2:38 PM, Kagamin wrote:

>

The very idea is strange. If RCSlice is mutable, how can it be immutable? Something doesn't add up.

Look up logical const, that is what this is. The mutable part never is immutable or const. We just also don't want it to be shared by default.

See also, C++ mutable keyword.

-Steve

November 12, 2021
On 2021-11-12 14:22, Steven Schveighoffer wrote:
> On 11/12/21 1:12 PM, Andrei Alexandrescu wrote:
>> On 2021-11-12 8:03, Timon Gehr wrote:
>>> On 12.11.21 13:31, Steven Schveighoffer wrote:
>>>>
>>>> I've come to the conclusion, in order to fix this situation, so `__mutable` really means mutable and `shared` is an orthogonal piece, you need to remove the implicit sharing of `immutable`.
>>>
>>> I agree, that would be much better.
>>
>> We discussed this a couple of times. It's interesting. Sadly at this point implicit thread sharing of immutable is so baked into the language, it would take a lot of care to extricate. It would be very difficult even for Timon or Paul.
> 
> I'm not going to fight for this, I just wanted to bring it up. But I think it's pointless to try for reference counting of immutable data (the long RCSlice thread), when it's obviously not immutable data.

I'm not so sure. I mean no laws of physics or even of typing are broken.

Consider as a baseline the following: there is a global synchronized hashtable mapping pointers to objects to pointers to reference counters. The table is obviously mutable. Whenever a reference to some piece of data is copied around, a function gets called that looks up the pointer to the reference count and increments it. Depending on type, the increment is interlocked or not.

This is correct code that typechecks and everything. The challenge is to make it faster. One common solution is to group together the memory for the counter (metadata) together with the memory for the object (data). For example, metadata can be placed before the data (negative offsets) or just after the data. Then the lookup becomes as cheap as a integral to pointer addition. Sure, that all does take some gnarly code that is not automatically verifiable, but the result is safe. All languages, no matter how safe, have such manipulation in their implementation.

The real challenge is figuring out exactly the minimum amount of change to the language specification to allow defining such a scheme.

> The only solution is to make it not mean the same as immutable does now (i.e. possibly shared). The fact that `shared(immutable)` would mean the same thing as `immutable` does now is a path forward, but I can understand if nobody wants to do that. I happen to think most cases people are not sharing their immutable data, just like they mostly aren't sharing thread-local data.

Though that may be a solution, I'm not sure it's the only solution. I can tell it's an expensive enough solution (in terms of changes to the language and/or broken code) that it warrants looking for another.
November 12, 2021

On 11/12/21 5:07 PM, Andrei Alexandrescu wrote:

>

On 2021-11-12 14:22, Steven Schveighoffer wrote:

>

I'm not going to fight for this, I just wanted to bring it up. But I think it's pointless to try for reference counting of immutable data (the long RCSlice thread), when it's obviously not immutable data.

I'm not so sure. I mean no laws of physics or even of typing are broken.

Consider as a baseline the following: there is a global synchronized hashtable mapping pointers to objects to pointers to reference counters. The table is obviously mutable. Whenever a reference to some piece of data is copied around, a function gets called that looks up the pointer to the reference count and increments it. Depending on type, the increment is interlocked or not.

I'm well aware of that concept. See this post I made from 2008 (!): https://forum.dlang.org/post/fsth4a$79l$1@digitalmars.com

The problem is of course that this is not pure. As long as you don't need pure reference counting, it's a possible solution.

If you do need pure reference counting, this mechanism cannot be used, even if it's not technically accessing a global. I don't care what tricks you want to pull, the optimizer is going to bite you for it.

I'm skeptical that even normal optimizations outside of pure won't bite you on this either.

> >

The only solution is to make it not mean the same as immutable does now (i.e. possibly shared). The fact that shared(immutable) would mean the same thing as immutable does now is a path forward, but I can understand if nobody wants to do that. I happen to think most cases people are not sharing their immutable data, just like they mostly aren't sharing thread-local data.

Though that may be a solution, I'm not sure it's the only solution. I can tell it's an expensive enough solution (in terms of changes to the language and/or broken code) that it warrants looking for another.

Understandable, altering a fundamental rule for immutable is definitely not the first choice I would have either.

-Steve

November 12, 2021

On Friday, 12 November 2021 at 23:07:02 UTC, Steven Schveighoffer wrote:

>

The problem is of course that this is not pure. As long as you don't need pure reference counting, it's a possible solution.

If you do need pure reference counting, this mechanism cannot be used, even if it's not technically accessing a global. I don't care what tricks you want to pull, the optimizer is going to bite you for it.

Not if it is built in. Anyway, you can just require all functions to take the counter table as a parameter, by rewriting rules, that makes it pure even when it isn't builin.

>

I'm skeptical that even normal optimizations outside of pure won't bite you on this either.

If it is builtin then optimizations cannot be a concern?

November 12, 2021

On 11/12/21 6:32 PM, Ola Fosheim Grøstad wrote:

>

On Friday, 12 November 2021 at 23:07:02 UTC, Steven Schveighoffer wrote:

>

The problem is of course that this is not pure. As long as you don't need pure reference counting, it's a possible solution.

If you do need pure reference counting, this mechanism cannot be used, even if it's not technically accessing a global. I don't care what tricks you want to pull, the optimizer is going to bite you for it.

Not if it is built in. Anyway, you can just require all functions to take the counter table as a parameter, by rewriting rules, that makes it pure even when it isn't builin.

That means every pure function must be passed the global table as a parameter just in case there are some reference-counting things to do, and that means that no pure function can be strong-pure. Might as well not have pure functions in that case.

> >

I'm skeptical that even normal optimizations outside of pure won't bite you on this either.

If it is builtin then optimizations cannot be a concern?

As I said, I'm skeptical, I'm not sure if some optimization somewhere isn't going to make hay with our cleverness. We have smart people working on the compilers, so I'll leave the judgment up to them.

-Steve

November 13, 2021

On Friday, 12 November 2021 at 23:50:15 UTC, Steven Schveighoffer wrote:

>

That means every pure function must be passed the global table as a parameter just in case there are some reference-counting things to do, and that means that no pure function can be strong-pure. Might as well not have pure functions in that case.

Might as well. So long as you're using (current spec of) GC, pure functions are a big fat lie. Not even because GC allocates, but because it runs finalizers, which can be impure. If a compiler makes any assumptions wrt. "pure" on a function that interacts with GC in any way, they're gonna be wrong assumptions.

November 12, 2021

On 11/12/21 7:27 PM, Stanislav Blinov wrote:

>

On Friday, 12 November 2021 at 23:50:15 UTC, Steven Schveighoffer wrote:

>

That means every pure function must be passed the global table as a parameter just in case there are some reference-counting things to do, and that means that no pure function can be strong-pure. Might as well not have pure functions in that case.

Might as well. So long as you're using (current spec of) GC, pure functions are a big fat lie. Not even because GC allocates, but because it runs finalizers, which can be impure. If a compiler makes any assumptions wrt. "pure" on a function that interacts with GC in any way, they're gonna be wrong assumptions.

This is an odd way to look at it. The finalizers are not run directly, and maybe not even run on the same thread as the pure function being run. They also should not be observable (for the most part), because you should not have access to that data any more.

It's like saying an OS context switch that happens in the middle of a pure function must somehow be valid pure code.

-Steve

November 13, 2021

On Saturday, 13 November 2021 at 03:03:21 UTC, Steven Schveighoffer wrote:

>

This is an odd way to look at it. The finalizers are not run directly, and maybe not even run on the same thread as the pure function being run. They also should not be observable (for the most part), because you should not have access to that data any more.

It's like saying an OS context switch that happens in the middle of a pure function must somehow be valid pure code.

-Steve

Not run directly??? As far as I know, GC, on allocation, may hijack the caller to do a collection. Net effect is that it executes arbitrary (not pure, not @safe, etc.) code within caller. Caller is marked pure. GC may run impure finalizers that mutate some global state. Something as stupid as call to "close" which may set errno, or, I don't know, freeing half of program state, mutating shared globals... There can be no "for the most part" here. You've got pure function mutating global state. That's something pure functions aren't supposed to be doing.
Unless that changed and GC isn't doing that anymore, that's a bug that's been open for some years now.

November 13, 2021
On 12.11.21 23:07, Andrei Alexandrescu wrote:
> 
> The real challenge is figuring out exactly the minimum amount of change to the language specification to allow defining such a scheme.

I think one reason we haven't been making progress on this may be that your intuition for the size of the minimum required amount of change to the language specification is somewhat too small.

You have stated requirements, some of them are impossible to satisfy, most of them will require a sizeable language change. We have to go through those problems one by one and address them in order.

You have to factor in that _a lot_ of the work is actually clarifying existing semantics and fixing issues there.

My suggestion would be to prioritize satisfying the requirements that do not relate to type qualifiers.


> 
> - be as close in semantics to T[] as possible, i.e. most code should be able to simply replace T[] with RCSlice!T and work unchanged

`T[]` is magic. We first need to make it possible to define a template in druntime that works as a drop-in replacement for `T[]`, even without reference counting.

> - manage its own memory by using reference counting

Easy, except for lifetime bugs in throwing constructors. Some hassle with default init.

> - work in pure code just like T[] does

Impossible, due to strong purity. `__mutable` would have fixed this.

> - work with qualifiers like T[] does,

Impossible. `T[]` is magic and `inout` is broken.

> both RCSlice!(qual T)

Easy.

> and qual(RCSlice!T)

Impossible. Would need an escape hatch for the reference count, which you have shot down before. `__mutable` would have fixed this.

Adding appropriate subtyping relationships between them also requires a new language feature.

> - work in @nogc code just like T[] does

Very easy.

> - work in @safe code just like T[] does 

Impossible. This has spawned DIP1000 and large amounts of follow-up work. And all of this work still does not solve the problem it set out to address. I think this is the main problem. It's not actually very important to support type/function qualifiers beyond `pure @safe @nogc nothrow`. The issue is that you cannot borrow out a `ref T`. This could be solved with some explicit language support, where the compiler tracks the lifetime of the reference and lets the container know once it's dead.
November 13, 2021

On Friday, 12 November 2021 at 23:50:15 UTC, Steven Schveighoffer wrote:

>

That means every pure function must be passed the global table as a parameter just in case there are some reference-counting things to do, and that means that no pure function can be strong-pure. Might as well not have pure functions in that case.

Depends on how you define pure.

Is it a tool for compiler optimizations? Not a problem. The compiler knows the invariants for reference counting.

Is it a tool to make it easier for programmers to reason about programs. Clarfy in the sec what you can assume.

Do you want the compiler to check strong purity? Define it. Make it possible to express it and let the compiler check it.

If you require pure to mean that no life times are extended, say so in the spec and let the compiler check it.

> > >

I'm skeptical that even normal optimizations outside of pure won't bite you on this either.

If it is builtin then optimizations cannot be a concern?

As I said, I'm skeptical, I'm not sure if some optimization somewhere isn't going to make hay with our cleverness. We have smart people working on the compilers, so I'll leave the judgment up to them.

An compiler optimization cannot create problems, by definition, as it does not change the language. If it creates problems then it means that the language specification is in need of fixing. Or that that the programmers assume things that the language does not guarantee.