you don't need the unsafe block, you can just use data.first().map(Cell::get).unwrap_or(0)
Why not? Because it would address one of the pain points of using Rust without removing normal Rust. Java can't deliver high performance with low resource usage like Rust can, and Go is fast enough but not as expressive as Rust. The combination of easy to use, expressive, and fast would easily attract adoption and also make it easier to bring teams on board. I'm also very confident that once it's available, more people will use the GC variant than the non GC variant. I'm currently building it, once it outperforms Arc, that becomes a hard sell
That's not quite accurate or detailed enough. More precisely, when copying a string, it doesn't copy the value via realloc, it copies the reference via a ref counter. Mentioning string copy is important here because it only happens with strings, not other types. So to summarize:
For everything other than strings, it's exactly the same as Rust (only strings produce ref counter assembly)
Except when copying a string, it copies the reference. In Rust, strings can't be copied, but manually call .clone() which copies the value via realloc then copy. That's fine too, at least it's not automatically realloc-copied
But what if a .clone_ref() method was provided that automatically copies the reference using a ref counter. The name could be .clone_rc() and .clone_arc() so can fine grainedly choose between rc or arc, and the type remains T instead of being wrapped into Rc<T>/Arc<T>, is that possible? Like Mojo, the type remains T
A garbage collector is anything that manages memory automatically through runtime tracking. Rc and Arc track it via runtime counter. Rust is not the only language that uses ref counting, Python's garbage collector is ref counting as its primary mechanism (the generational GC only cleans up what ref counting fails to handle, such as circular references)
There's a mistake, ref counting in Mojo doesn't apply to all ref types. It only happens when you return like this:
// a is &String by default, so the reference is first class, where in Rust pass by move is first class
// you cloud annotate the a manually with ref a, but it is not needed
// for function parameters, the default is already &T
// the return value default is T, so only need typing ref in return type
(a: String) -> String:
return a
Instead of making it a compile error saying "can't copy/move String from a reference parameter String", it makes it copy the reference using a ref counter
If you write:
-> ref[origin] String:
return string
That's normal like Rust
Or if you write:
-> String:
return string^
That becomes a move and only succeeds if the parameter is not a reference but a var which can pass ownership
So it's incorrect to say that ref counting happens on all ref types, it only happens with strings, and if you explicitly write ref it won't use the copy reference by ref counter. In Mojo you don't need to annotate the function parameter to get &T. But you need it for return type, and variable declaration, like:
var a = "abc"
ref b = a
@CoolSchnoodle The consecutive reply limit is 3 messages, so I'm responding to your message by extending the last comment as suggested by the error message
That misses the point. The point isn't that I don't know how to make it autovectorize, I was showing the downside of those constructs in that they prevent the auto vectorization optimization pass that would otherwise succeed if they weren't present in the input code of the autovectorizer. You moved it outside the loop, whereas the input code for auto vectorization is the loop body itself. Auto vectorizing a loop will fail if there's a jump instruction inside it that the compiler fails to remove or move to outside loop, and I'm showing that RefCell and Cow are among the things that introduce that. Then you suddenly show Cell (which is not RefCell, it doesn't have what RefCell has because it only works with types that implement Copy, i32 implements Copy, try replacing it with Cell<String> it won't work, whereas RefCell can take a reference of complex data types like String, Vec, etc), and move the Cow outside the loop, which is not the point because it doesn't happen inside loop, so it doesn't fix the downside, but dodge the downside. Because what was being the problem is the downside of RefCell and Cow when called inside a loop. Unless you're demonstrating how to keep auto vectorization working with RefCell and Cow inside the loop
You don’t need to convince most people that good things are good and One Language to Rule Them All would be good.
One does not simply walk into an extremely high-performance tracing garbage collector with a multithreaded mutator, low-cost C FFI, no peanut butter overhead, and good ergonomics. D can’t, and Rust has more in common with D than than it has with, for example, Go.
No, what I mean is: if Rust had a GC on par with the JVM's, I could still leverage Rust's expressive type system for zero cost abstractions (generics and traits), and it also makes it much easier to write non performance critical applications in Rust, without switching languages. Go is verbose and limited in expressiveness, Java is verbose and resource heavy. Neither is a great alternative, I want to use something that is easy, expressive, fast, and low resource usage
Isn't the pointer part could be something like a dedicated pointer type, say, std::ptr::Gc, that opts into GC managed memory, with the option to fall back to raw Box RAII semantics via a --no-gc flag. There are already GC languages that support high performance C FFI, C# for example
The JVM and CLR have compacting GC. Compacting GCs need to move managed objects and update all the pointers to them.
Existing Rust code is built around & references, so if you want a GC that can easily be used in existing code, you’ll end up reborrowing Gc<T> -> &T to use it, just like you already do with Rc<T> and Arc<T>. The noalias optimizations and unsafe code guidelines don't allow the object behind a & reference to be moved, so compacting GC has to be prevented from moving an object as long as a & reference to it exists.
I’ve used the mozjs bindings, which does this in a library. The ergonomics are not as good as writing in a GC'ed language.
If you don't mind the GC being not quite automatic, you can solve this by having the compaction be something that is requested manually. The Gc<T> to &T conversion can require a shared borrow on a "garbage collector" object, and the compaction can require a mutable borrow on the same object, proving that you don't have any Gc<T>-derived &T when you perform a compaction.
I guess it depends on what you're trying to accomplish by adding a GC. If your goal is to let people ignore the borrow checker, requiring them to borrow the GC defeats the purpose.
Yeah, I'm aware of the compacting in JVM GC, I mentioned it in my earlier comment. I haven't gone into the implementation details yet because I'm still evaluating tradeoffs, particularly around which approach has the lowest cost without causing continuous writes like reference counting does
There are still open design questions I'm working through, whether to preserve aliasing xor for data race prevention and optimizer hints, or avoid it for real flexibility like a real GC language, and whether to go with per thread GC or a shared GC with background thread
For stop the world, the current thinking is to use a proc macro to insert function calls at infrequent, safe points that check a global flag, if the condition is met, the thread sleeps and the GC takes over
The part I'm less satisfied with is, having &T point to an entry in a global pointer table rather than directly into the arena allocator's raw buffer, essentially &*mut T transmuted to &T, with a custom Deref doing a double dereference under the hood. But this adds +1 indirection to every heap access, which I'd rather avoid. Or avoid reference by using custom pointer type fully
@JarredAllen @pitaj @SkiFire13 @RalfJung This to me is a short coming of Rust! It is special cased to compile without a lifetime if there is only one input reference. Such exceptions do not sit well with me. It is beyond me why this is not generalised as: “An output reference’s lifetime is inferred as the shortest lifetime of all input references. When that is not what you want, you must provide an explicit lifetime.”
I don’t think this would would break backward compatibility. It seems to me like a pure win, of making lifetime elision more consistent.
A pedantic lint and in edition 2027 it could become a warning, if you provide such, then superfluous, lifetimes.
That would be inconsistent with the rule that if there is a self param, the lifetime of this self param would be chosen. This rule is what you want most of the time, not having this rule would be a breaking change even with your generalization as it would lead to many output references getting a shorter lifetime that may not work for the caller. Lifetime elision works for the obvious cases. For the less obvious cases whatever rustc would pick will either break the function itself or be needlessly restrictive (and possibly even make the function entirely impossible to use) for callers.
Oh, another exception, albeit a very useful one. I didn’t have on the radar that we already have two cases.
We must, of course, keep that one, and could still generalise the other case, when none of the references are &self. Special casing &self, doesn’t imply that we need an arbitrary restriction on non-methods.
That sounds bad. Then the output lifetime for:
fn find_substr(haystack: &str, needle: &str) -> Option<&str> {
haystack.find(needle).map(|idx| &haystack[idx..])
}
changes based on whether needle lives longer or not when it should always be haystack's lifetime. I like the current rules:
- if there is one lifetime, use that
- if there are multiple, but one is a receiver, use its lifetime
as they're pretty straightforward and easy to infer directly from the signature. Determining "shortest" requires considering the calling context and it can change from caller to caller.
Because most of the time that's not what you want. Often you'll have a "main" struct/reference from which you'll borrow the return value from and potentially other helper structs/references can you'll use to determine what to return, but that are not actually borrowed by the return value.
The rule for the return lifetime of methods defaultinf to the lifetime of self is a realization of that observation, combined to the fact that the "main" lifetime in those cases is self. In non-methods there are no precise rules for identifying the most-likely "main" reference, so we have no such rule.
What you are suggesting will reduce the number of function requiring lifetime annotations, and will make in general more functions compile, but it will most likely result in compile errors when you call those functions that now compile, because the elided lifetimes are most often simply incorrect, and that's a much more confusing error than the one in the definition.
There's no such thing as a compiler detecting when that is not what you want. You'll have to detect that, if your Rust knowledge is advanced enough, instead of being prompted by the compiler like it happens right now.
That interpretation isn't really workable,[1] at least, not if the behavior is based on the signature alone and doesn't introduce arbitrary relations between the input lifetimes. The only reasonable and workable interpretation IMO is that the intersection of the input lifetimes is returned. Or in more practical terms, that the return value always keeps both *haystack and *needle borrowed.
I don't think that is desirable,[2] and would oppose such elision behaviour, but it's workable for the example[3] and not totally arbitrary.
Same thing, more words
Lifetimes do not have a total order. Though we use terms like "longer" and "shorter", the relations between lifetimes are more akin to subset relationships. Thus given two unconstrained input lifetimes 'h and 'n, it may be the case that neither 'h: 'n nor 'n: 'h hold. Or in other words, this function:
fn find_substr(haystack: &/*'h*/ str, needle: &/*'n*/ str) -> Option<&str> {
// Behaviour not based on the body as that would destroy the usefulness
// of function signatures being borrow checker boundaries
}
cannot have the behavior "sometimes returns 'h, sometimes 'n, never anything else".[4] At least, not without arbitrarily introducing lifetime relations between the input lifetimes, or throwing errors for any function body that doesn't return something 'static.
The closest reasonable behavior to "return the shortest" that could be based on the signature alone is probably introducing another lifetime which is (within) the intersection of all input lifetimes.
fn find_substr<'h, 'n, 'i>(haystack: &'h str, needle: &'n str) -> Option<&'i str>
where
'h: 'i, 'n: 'i // "'i is [within] the intersection of 'h and 'n"
{ ... }
(Using the same lifetime for everything is similar when all input lifetimes are covariant, but not elsewise, and doesn't really jive with the description "return the shortest lifetime".)
Though again I don't really find this a reasonable default for elision; it's certainly incorrect for the example at hand.
by which I mean, if the lifetime returned could vary based on the call site, the function body should throw a borrow checker error for this example ↩︎
keeping all inputs borrowed is ususally incorrect for real-world use cases IME ↩︎
due to covariance; it would be awful in other cases ↩︎
The practical implications being "the return value sometimes keeps
*haystackborrowed and sometimes keeps*needleborrowed". ↩︎
Note that there's history here. In the early days of Rust, before 1.0, there was a period of time where there was no lifetime elision at all. In other words, you could not write
fn foo(bar: &str, baz: usize) -> &str
because the compiler could not infer lifetimes at all. Instead you were forced to write it as:
fn foo<'a>(bar: &'a str, baz: usize) -> &'a str
The current lifetime elision rules came from observing code that had fully explicit lifetimes, and recognising that there were common patterns.
First was the "if there's only one parameter with a lifetime, then the output lifetime has to be the same as the input lifetime", which covers my foo example above - there's only one parameter with a lifetime, so the output lifetime must be either the input lifetime, or the special case 'static.
Second was the observation that functions with a receiver generally want to use the receiver's lifetime as the output lifetime. In other words, if you write lifetimes explicitly, you're significantly more likely to write:
fn foo<'me, 'param>(&'me self, param: &'param str) -> &'me str
than
fn foo<'me, 'param>(&'me self, param: &'param str) -> &'param str
and therefore it's worth treating the first form specially.
There's no reason to not add more elision rules - but they should be justified the same way the current rules were, by showing that there's a significant body of code that currently has to use explicit lifetimes, but that would be quite clear with inferred lifetimes if you followed this unambiguous rule.