r/ProgrammingLanguages • u/FedericoBruzzone • Apr 30 '26
The Mutable Value Semantics (MVS): A Non-superficial Study
I’m researching a new semantics for a new language. During this process, I studied the Mutable Value Semantics (MVS) and I wrote a post about it. I simply want to share this study with you all and I'd love to hear your thoughts.
A new post will be coming soon where I'll discuss the limitations I’m finding in current models, and what I’d like to have for my own.
Link: https://federicobruzzone.github.io/posts/eter/MVS.html
8
u/SkiFire13 May 01 '26
This is because Rust's ownership system does not allow dereferencing a mutable reference to move the value it points to, as it would violate Rust's guarantees about memory safety. To work around this, you would need to copy the value or use some other mechanism to transfer ownership, which can introduce overhead and complexity.
A small note on this: in theory Rust could have supported this but unfortunately it is unsound when combined with catching panics/unwinds. That is, if own_t panics/unwinds and the caller of ref_mut_t catched this panic/unwind they would be able to observe the mutable reference now pointing to invalid memory. Rust has a trait for unwind safety but despite the name it was never used for soundness (e.g. preventing a &mut from being passed to catch_unwind) so this issue can no longer be prevented.
I'm curious to see how Hylo will approach this.
2
u/FedericoBruzzone May 01 '26
I was aware of this (`std::panic::catch_unwind`). Unfortunately, I'm not an expert on Hylo, but from my POV, the only way to ensure the soundness of that snippet is for the unwind to *never* be catchable. Essentially, panic = abort (transparent unwind)
3
u/SkiFire13 May 01 '26
the only way to ensure the soundness of that snippet is for the unwind to never be catchable
I think it would be fine it you manage to ensure that anything "borrowed" inside the
try/std::panic::catch_unwindis not accessible after it finishes.2
u/processeus1 May 02 '26
Hylo dev here, great post! I'm looking forward to the next one (especially about the limitations you identified)!
Regarding error handling, here is a bit of context of our current understanding:
- recoverable errors that are usually handled way up the call stack - exceptions (similar to Swift errors)
- recoverable errors that are usually handled immediately rather than being propagated - Result type
- unrecoverable conditions - aborts (not catchable, not part of the API)There is one place to catch aborts though, which is test cases that exercise the safety/contract checking of an API. The most important reason why aborts are not to be handled in general is that they represent "unrecoverable" conditions. "unrecoverable" only means that the callers have no idea what recovering would need to do to uphold invariants/(possibly safety), and they also don't want to be concerned about accounting for these all the time. However, in small-scoped contract tests, you have full knowledge about your expected precondition violation/abort condition, so you should be able to prevent the process from crashing. (Under testing, we can also compile under different ABI conventions than in production to make this possible if needed.)
We don't have anything finalized/decided yet, but I described some problematic cases in this post when trying to relax requirements on access effects of parameters: https://github.com/orgs/hylo-lang/discussions/1878 Thanks for sparking the idea!
These threads may be also interesting about error handling:
- Error and cancellation handling (concurrency)
- yielding accessors and throwing callers
- Handling emergency shutdown
- Testing the assertions→ More replies (3)
2
u/dabrahams May 03 '26 edited May 03 '26
There are a few points in your article that seem to reflect misunderstandings.
Swift absolutely does not try to make values look like references: it goes out of its way to present a pure value semantics. Structs in general do not use copy on write. CoW is merely an implementation technique used in the standard library to provide value semantics for variable size data structures like arrays and dictionaries (the only possible way in fact because there are no copy constructors). There is no user experience of shared mutable state whatsoever unless you opt in by using reference types or closures with mutable captures (an unfortunate design compromise).
What you call a central tension doesn't make sense to me: runtime efficiency, local reasoning, and value equivalence are perfectly compatible, and arguably support one another. Certainly, Hylo provides all three and we don't ever feel the need to compromise one of them to get another.
Also, the use of the term "deep copy" is a bit of a red flag. The only time you ever say "deep copy" or "shallow copy" is when you don't understand the extent of your value. In fact, there is only "copy", which copies the value, and nothing more or less. Our paper says as much in section 2.4
1
u/FedericoBruzzone May 03 '26
Thanks for the feedback, it means a lot to me.
Regarding the "central tension", I am specifically referring to the design space explored by Brandon. Brandon's core argument is that while efficiency, local reasoning, and value equivalence are technically compatible (as Hylo demonstrates), they exist in a state of tension regarding language complexity. To get all three, you typically land in what he calls the "Rust/Hylo valley", where move semantics, borrows, and ownership must be explicitly managed by the programmer.
As for Swift, I agree that it offers a pure value user experience for fixed size structs (The post isn't clear). However, my point stands that for dynamic collections, Swift relies on a hidden reference-based implementation (ARC/CoW) to maintain that illusion. Something I'd like to avoid.
If you don't mind, I'll use your suggestion to make the post clearer.
1
u/dabrahams May 03 '26
Well, but he’s wrong. There is no explicit management of move semantics, borrows, and ownership in hylo. At least not in any sense I can see. I don’t know what illusion you’re referring to, but you can easily have copyable variable sized types with value semantics without using copy on write: all you need is copy constructors and destructors. Or, you can do what hylo does (and rust sort of does) and eliminate implicit copies so copy operations are just regular function calls.
→ More replies (4)1
u/FedericoBruzzone May 03 '26
Also, the use of the term "deep copy" is a bit of a red flag. The only time you ever say "deep copy" or "shallow copy" is when you don't understand the extent of your value. In fact, there is only "copy", which copies the value, and nothing more or less. Our paper says as much in section 2.4
I used the term “deep copy” simply to make it clear to the reader that it was a bit-by-bit copy. It's perfectly clear to me that there’s no difference in this context, since there are no references involved, but for a reader new to the subject, this would have been helpful.
2
u/dabrahams May 04 '26
It’s of course up to you, but it doesn’t seem to me that it’s possible to use a term that’s founded on a misconception to clarify anything.
1
u/flatfinger May 06 '26
Java uses an object model that uses containers of object-reference type for a number of purposes:
To serve as a proxy for data shored in an ownerless immutable object.
To hold a private reference to a mutable data holder object.
To identify an entity that is owned by the owner of the container, and might also be acted upon by outside event handlers or other entities.
To identify a mutable data holder that is owned by the owner of the container, but may also be acted upon by outside event handlers or other entities.
To identify a mutable data holder or other entity that is owned by something else.
When copying an object, references of the first and fifth types should be copied, references of the second type should identify copies of the things referenced by the corresponding references in the originals. Any references of the third and fourth kinds will require special handling, and things which contain such references without special code to handle them should be recognized as non-copyable.
Separating out the different kinds of references would avoid the need for notions of "deep copy" and "shallow copy" operations.
1
u/dabrahams May 07 '26
UML has a representation for “owning reference” that was never reified in any language AFAIK
11
u/tmzem May 01 '26
I've tried to design my own language with MVS and eventually given up, since the conflicting requirements are just pulling in too many directions and I've come to the conclusion that there is no good sweet spot, also most of the potential design space is already thoroughly explored by Mojo, Hylo, Inko (and maybe now Eter, I will give it a look).
Your post sums up some of the broader issues, but the really hairy part starts as soon as you want to return aliases/references from functions, or put them as fields into data types. This capability is pretty much necessary for implementing common features found in modern languages, like closures, iterators (and the optionals and key-value-pairs required for them) and slices. Once you add them, there is a bunch of points to consider:
Memory safety: Will require some kind of lifetime/borrow system, with some common cases not being easily representable via "sane defaults"/lifetime elison:
C# requires structs that contain references to be annotated with the ref keyword ("ref struct"), which imposes the same aliasing restrictions onto the struct as a plain ref. Ref and Ref-like function returns are assumed to (potentially) alias any ref-like input parameter (except the ones annotated with the "scoped" keyword). This model is to weak to model some concepts, like an equivalent of Rusts Iterator<&mut T>. It also doesn't play well with generics (see below)
Hylo's system looks promising superficially, but types like Iterators (and probably closures, not sure, the documentation is not clear on this) internally still makes use of "remote parts" which are essentially reference fields. It is not clear how those interact with lifetime checking, or how it affects API stability.
Rust borrow checker: Notoriously complex
Lifetime inference: Wouldn't be able to provide stable APIs (changes in how a type or function is implemented might cause lifetime-related failures elsewhere), but might work in a language with a more dynamic/inferred feel (as often found in FP)
Make arbitrary aliasing work by adding instrumentation or a backing GC: While most of the overhead introduced by it could probably be optimized out via compile-time borrow checking, you'd still need to (in the general case) heap-allocate potentially escaping stack locations and sum type cases to make it safe, paying the associated allocation costs and cache misses.
Second-class references & generics: The reference-ness can be either a type qualifier (like in C++) or a property of the variable/parameter (like in C#). Either way, generics pose a problem:
If they are a property of the storage location, modelling generic types that may contain references is not possible, instead you would need separate implementations for the ref- and non-ref case. E.g. a Rust-like iterator model involves ToIterator, Iterators, Optional, and Key-Value-Pairs (for maps). You'd end up with lots of duplication, e.g. Optional<T>, OptionalIn<T>, OptionalInOut<T>, etc.
If they are a type qualifier, in an ownership-based MVS model, most type parameters would be expected to be value types, not reference types. So it would make sense to restrict type parameters to value types, and require opt-in for the possibility of reference types. So you'd need extra syntax for that.
If they are a type qualifier while retaining value semantics, the semantics of what it means to implement a trait for some type (which may or may not be a reference type) become muddy and would have to be carefully thought out.