So Rust has global references ownership rules, which only allow one (borrowed) mutable reference to the same object to be active in the current block scope. The objective is to prevent race conditions on the object. But from the discussion we had on the forum, a DAG tree of disjointness is not a general model of imperative programming and afaics in the general case requires dependent typing (which is known to exclude Turing-completeness). Thus I abandoned Rust's global borrowing rules, because it seems like a pita of false promises that is rarely (20% of the time?) applicable.
But I proposed a more localized use of borrow tracking which I also referred to in later posts{1} that afaics can help free temporary objects without relying on GC in many cases.
I don't intend to use my proposal to prevent race conditions due two or more borrowed mutable references existing in the same block scope (which as I stated above Rust prevents and which I think is a PL design error). In the case where all these borrows are "outer-only" immutable, my proposal can guarantee the owner has an exclusive mutable copy of the reference w.r.t. modifying the polymorphic type of the reference. This I think can be useful for the case where we want to assume the owner has the only copy of a reference to the collection in its block scope, so that my first-class unions idea can be used to add new element types to a collection without needing to require that the collection is an immutable data structure (since immutable data structures are generally at least O(log(n)) slower). In other words, we can force immutable access on a mutable data type every where except for the owner's block scope, so this gives us the best of both mutable data structures and immutability for prevent race conditions.
I was thinking about how asynchronous programming (e.g. yield
) impacts my proposal. For example, in JavaScript yield
of a Promise
will allow the current thread to run another stack which was waiting on a Promise
event which has been fulfilled and is at the top of the event queue. In this way, JavaScript is able to achieve concurrency without the slower overhead and bugs of synchronization primitives such as mutex guards. But this form of asynchronous concurrency can't create race conditions in my proposal, because the stack is not made reentrant, i.e. each asynchronous DAG of execution is on a separate stack. And my proposal will not pass borrowed references between threads of execution (in either form, neither OS threads nor asynchronous DAG of execution).
So I conclude my proposal is robust.
{1} [quote=https://users.rust-lang.org/t/inversion-of-control-resource-management/5737/41] 3: GC every where except ability to use borrow checking to track lifetimes of temporary objects to free them without burdening the GC in many cases. And copying some of v8's techniques for improving the robustness of handling temporary objects for those cases that fall through to the GC. [/quote]
[quote=https://users.rust-lang.org/t/inversion-of-control-resource-management/5737/51] Remember upthread I mentioned the idea of possibly employing borrow checking in a very limited way (that would be silent no annotations in most cases) which didn't extend its tentacles into a total ordering, yet afaics might enable to declare some references to be unique (non-aliased) mutable, so that these could be compile-time deallocated (not given to the GC) and afaics these guarantees would also eliminate the need for the immutable restriction on adding elements to collections under my proposed complete solution to the expression problem.
Thus I think the GC would be entirely out of the picture when interface to FFI in that case, but I need to dig into the details to be sure. [/quote]
[quote=https://users.rust-lang.org/t/inversion-of-control-resource-management/5737/37] I do roughly envision a possibly very significant advantage of compile-time checking the borrowing of memory lifetimes local to the function body, if that can help offload from GC the destruction of temporary objects and if it won't require lifetime parameters. And that is because thrashing the GC with temporary objects appears to be one of the most significant weaknesses of GC. That is only enforcing a partial order, and not attempting to compile-time check a global consistency which can't exist. In short, I don't want a type system that silently lies to me very often. [/quote]
[quote=https://users.rust-lang.org/t/inversion-of-control-resource-management/5737/28] That is why I am pondering about localizing the borrow checker to just function bodies, so that we can get some of its benefits without having lifetime checking grow tentacles into everything where it can't even model general semantics. [/quote]
@shelby3 wrote:
If as in my proposal we are deciding to use GC pretty much every where it isn't simple (localized, i.e. no lifetime parameters) for us to track lifetimes, then it seems (virtually) all moves are subsumed by GC references, except where we need to move a mutable reference to another OS thread. Seems in my proposal we only need to track moves for that case of giving a mutable reference to another thread. And these mutable references could I suppose be either GC or lifetime tracked.
@keean wrote:
With your feedback I see yet another confirmation that immutability is generally slower. So immutability every where is not desirable.
However there is still a performance benefit to tracking immutability. For example, if we are doing some paritionable operation on a collection such as fold, map or even iterators, and if we know we don't modify any existing elements of the collection, then we can parallelize the computation. We can't safely parallelize if we modify the elements because we don't know if the elements have a circular reference to the collection that contains them and thus whether mutating them thus modifies the collection we want to partition over.
Rust tracks every reference and thus can guarantee that elements can't have a mutable reference to their container, but this requires a global lifetime tracking with lifetime parameters which I've already criticized.
Yeah it seems to me we need finer grained immutability tracking:
So thus if your iterator library restricts against permission to invoke certain trait bounds, then no other borrowed reference in the same (or derivative) block scope could use those trait bounds. I think that would entirely solve the problem without Rust's conflation of immutability with the global consistency (no partial orders allowed) and Rust's conflation of the different aspects of an object that programmers want to enforce immutability on.
The key distinction is instead of trying to track allocation lifetimes every where they go (with lifetime parameters), we instead only track allocation lifetimes for borrows that don't leak into return values and everything else gets delegated to GC. And for mutability we only track borrows that don't leak into return values and we track 3 different aspects of mutability, so that we can have more than one mutable reference to the same collection for example. For mutability that leaks into return values, we punt and require these to be fully mutable so the programmer won't be able to enforce immutability in that case and needs to restructure the code if necessary to enforce immutability. I would need to look at examples to see if this is 80/20. But it can't be worse than a language that doesn't track immutability at all, e.g. most every popular language (other than Rust and Haskell, but these aren't popular).
So I am NOT proposing that allocation lifetime and mutability checking are separate concerns, because a GC reference can be copied any where so it leaks, thus only a borrowed function argument can have mutability restriction annotations.
Thus I modify my proposed syntax as follows.
Note that normally adding a new element type to the first-class union type parameter can't be type checked in a programming language unless the data type is a single-linked list, equivalently immutable data structure, or the data type is copied. Thus otherwise the only way to achieve adding a new element type to the first-class union type parameter, is to have an annotation for a method that declares it can change the polymorphic type parameter of the data type it is mutating, e.g. for arrays, vectors, doubly-linked lists, etc.. So we need a
modifies
prologue on the function declarations stating the new type of the type parameter(s) ofself
that results. These need to be methods on thestruct
nottrait
interfaces, so they can be enforced (see mention of modules below).let
,val
for entirely immutable (rare) andvar
for partially or entirely mutable.var
. Prepend withval
to make them entire immutable (rare).modifies
methods. I suppose we might want to be able to track mutability of each type parameter separately, but for now I am going to simplify and assume we lump all together so we can infer it. Note this inference has to be transitive to all trait implementations which call thesemodifies
methods.#
.where
clause anx: !TraitBound1|TraitBound2|...
.Type
and a GC reference (hopefully rarer) will be::Type
. So when we write these as a function definitions (not just a signature declaration in a trait), we writex: Type
andx ::Type
respectively. This terse syntax is chosen to cut down on the noisy verbosity, as well to enforce a consistency of spacing style to improve readability. Having the compiler check the spacing and the single or double colon, is a double consistency check against typos. When writing a local GC variable and we want to infer the type, we write for examplevar x ::= ...
.In order to give the collection designer the control to insure that others can't create new trait implementations that, or otherwise directly, modify the data type so as to subvert the above mutability protections, we must have modules so we can restrict which traits have access to which capabilities.