Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changed return_ref syntax to returns(as_ref) and returns(cloned) #772

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

CheaterCodes
Copy link

As discussed in #719 , I figured I'd give this a go.
It was about as straightforward as I expected, with about as much problems as I anticipated.

Basically, I replaced the return_ref syntax with returns(as_ref). This allows other return modes to be specified as well, like the current default of returns(cloned).

I used returns instead of return, because using #[return(ref)] already fails pre-macro expansion, since return is a keyword. I used as_ref instead of ref, because it required less rewrite of the macros. It's possible to use ref, but I didn't bother with it for now. I used cloned instead of clone, since it felt more analogous to as_ref (compare with Option::as_ref and Option::cloned).

Since this was my first time browsing this code base, I wasn't entirely sure how hacky is too hacky. I think it's not too bad, other than the reuse of maybe_clone (which I renamed to return_mode), since I didn't want to duplicate the code.

I don't necessarily expect this to be merged, but I wanted to share the results of my experiment.

I also didn't change the defaults away from clone yet, since updating all the tests seems like an annoying chore that I'd rather only do once it's decided on.

Copy link

netlify bot commented Mar 22, 2025

Deploy Preview for salsa-rs ready!

Name Link
🔨 Latest commit a2a48eb
🔍 Latest deploy log https://app.netlify.com/sites/salsa-rs/deploys/67e7e25b3ac1ea0008e0d8da
😎 Deploy Preview https://deploy-preview-772--salsa-rs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

codspeed-hq bot commented Mar 22, 2025

CodSpeed Performance Report

Merging #772 will not alter performance

Comparing CheaterCodes:return_ref (3e9b71f) with master (6cfe2aa)

Summary

✅ 12 untouched benchmarks

Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change itself looks good to me. However, I realised that it isn't entirely clear to me how we'd implement as_deref because it would require knowing the return type (Option<Name> where Name has a Deref implementation to str returns a &str and this isn't something that salsa can easily infer).

That makes me think that it might be good to add as_deref support as part of this PR, to ensure that the new syntax is flexible enough (I don't want to make this migration twice 😅)

A possible syntax could be returns(as_deref = &str)

@CheaterCodes
Copy link
Author

I would have assumed it would work the same way Option::as_deref does; It would only deref once to &<T as Deref>::Target.

Honestly, I didn't even consider Option, but I realize now what you wanted to do. Currently as_ref would also return a &Option<String> and as_deref would fail, since Option doesn't implementDeref. So maybe returns(ref/deref) and returns(as_ref/as_deref) would be different?

Another option I considered is to instead implement something like returns(&str via |x| x.as_deref()), but that feels maybe a bit much?

@MichaReiser
Copy link
Contributor

I would have assumed it would work the same way Option::as_deref does; It would only deref once to &::Target.

Yes, it should work the same as Option<Deref>.

Oh, I see, yes my example was incorrect. It should return Option<&str> given Name implements Deref<Target=str>. But also, yes, we may need more variants because we want to support returning a slice instead of a &Vec (deref) and also returning a Option<&T> instead of &Option<T> (as_deref)

Hmm, we might need to think a bit more about the desired API. It would be nice if we didn't have to special case Option::as_deref and could instead have a generic API for going from &T to the desired return type and cloned, deref, as_deref would just be well known cases. Either way. Supporting copied, cloned, as_ref, ref, deref and as_deref seem a good starting point. I'm still wondering if there's a way to not require an explicit return type for as_ref and as_deref (a custom salsa trait?)

@CheaterCodes
Copy link
Author

Just to clarify on ref: it's slightly annoying because it isn't a valid identifier and half the macros assume that. but I think it's doable, I'll give it a shot.

From my quick browsing of the std-lib, Option and Result appear to be the only types with an as_ref and as_deref method that isn't covered by deref already.

So we could special case it, or, like you suggested, use a custom trait. That may also be able to cover other custom mappings that a user might want to provide.

pub trait AsDeref {
    type Target<'a>;

    fn as_deref(&self) -> T::Target<'a>;
}

should in principle cover all allowed transformations (not just as_deref).

@CheaterCodes
Copy link
Author

This would be a lot easier with spezialitation 😅

@Veykril
Copy link
Member

Veykril commented Mar 23, 2025

I think you both already agreed on it but I'll just put down my coarse view on it (partially rehashing what was already said)
We have the basic ones which are trivial to implement

  • copy: just leave the return type verbatim and dereferencing the returned borrowed
  • clone: likewise but with .clone()
  • ref: put a &$db_lt in front of the type
    The following require traits to transform the return type
  • deref: Change return type to &$db_lt <Ty as Deref>::Target, put call ::core::ops::Deref::deref(ret_val) on return
  • as_ref: Here a question comes up, do we mean the AsRef trait? Or do we specifically mean functor like things like Option::as_ref, Result::as_ref? Probably the latter, the former seems not as useful.
    • in the case of the latter, I'd expect us to offer a custom trait
    trait SalsaAsRef {
    	type Out<'a>;
    	fn as_ref<'a>(&'a self) -> Out<'a>;
    }
    and do the transformation as with deref for the return type. The trait just delegates to Option/Result::as_ref as the provided impls of it,
  • as_deref: Same as above really

Naming bikeshed: Should it be copy / clone or copied / cloned?

Hmm, we might need to think a bit more about the desired API. It would be nice if we didn't have to special case Option::as_deref and could instead have a generic API for going from &T to the desired return type

This sounds generally nice to have and I think can also just be modeled with a simple trait that we can provide a derive for which would basically look the same

    trait SalsaReturnTransform<'db> {
    	type Transformed;
    	fn as_ref(&'db self) -> Transformed;
    }

(I think a GAT would be less flexible here than tying the database lifetime to the trait impl, not 100% sure though)
then we could just have an opt-in for that like #[returns(transform)]/#[returns(transformed)] (with the right spans the IDE should even be able to jump to the trait impl on the transform ident there 👀)

@CheaterCodes
Copy link
Author

CheaterCodes commented Mar 23, 2025

Seems about right, yeah.

copy: just leave the return type verbatim and dereferencing the returned borrowed

I would rather wrap the value using something like copy(x) where fn copy<T: Copy>(x: &T) -> T, I think that gives better errors. Same with using Clone::clone(x), and like you did with Deref::deref as well.

as_ref: Here a question comes up, do we mean the AsRef trait? Or do we specifically mean functor like things like Option::as_ref, Result::as_ref? Probably the latter, the former seems not as useful.

I agree, it should be the latter, via a custom trait. AsRef doesn't make much sense because it is generic (we'd need to specify T), and it would leave us with missing Option::as_ref again, which I think is important. Also, most types where we'd want as_ref already have a Deref impl anyway. I suppose you could do something like as_ref([u8]), but that may be a bit confusing if as_ref alone means SalsaAsRef::as_ref.

Naming bikeshed: Should it be copy / clone or copied / cloned?
Probably copy/clone. I think the other option would be confusing with Option::cloned/Option::copied, which I guess would be a completely separate thing again.

Regarding transform, I think a GAT is the correct model here, but I'm also not sure. Would be great if someone researched this!
I also considered if something like transform([u8], |x| x.as_ref()) could make sense, but at that point it's probably better if the user just writes a getter for this.

Also there still needs to be a decision on the default: I believe that ref is the most sensible default, since it does the least magic under the hood, at least for struct fields. For tracked functions, I could be convinced to make the default copy instead to avoid implicitly rewriting the signature as a default. I could maybe be convinced that copy should be the default for both, but I'd really like ref for struct fields.

@CheaterCodes
Copy link
Author

I have now implemented copy, clone, ref, deref, as_ref, as_deref.
There are two new traits salsa::SalsaAsRef and salsa::SalsaAsDeref for dealing with the last two cases. These can be implemented by crate users, so in principle that already deals with the custom transform use case.

Once we've decided on the default (I'm still undecided between copy and ref), I will go through and update the tests and examples, using copy, clone, ref where appropriate, probably deref for Vecs and Strings. It seems like as_ref and as_deref would require separate tests; There aren't really any Option<BigStruct> or Option<String> going around it seems.

Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. This looks good to me. I'd prefer if someone else good quickly glance over it to confirm that they agree with the overall approach.

Could you add a test that demonstrates thtat the following doesn't compile: returns(not_a_return_mode).

I'd be okay if we switch the default in a separate PR

///
/// Used when generating field getters.
#[macro_export]
macro_rules! return_mode {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe return_expression and return_expression_ty or result_ty and result_expression?

Comment on lines +1 to +3
/// Generate either `field_ref_expr` or a clone of that expr.
///
/// Used when generating field getters.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation here needs updating


use std::ops::Deref;

pub trait SalsaAsRef {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to have some more documentation for those public types

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I try to avoid traits modules because it suggests that all traits are in this module (they're not). Maybe return, as, return_mode?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants